001package org.hl7.fhir.r5.renderers;
002
003import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
004import static java.time.temporal.ChronoField.YEAR;
005
006import java.io.IOException;
007import java.math.BigDecimal;
008import java.text.NumberFormat;
009import java.time.LocalDate;
010import java.time.ZoneId;
011import java.time.ZonedDateTime;
012import java.time.format.DateTimeFormatter;
013import java.time.format.DateTimeFormatterBuilder;
014import java.time.format.FormatStyle;
015import java.time.format.SignStyle;
016import java.util.Currency;
017import java.util.List;
018
019import org.hl7.fhir.exceptions.DefinitionException;
020import org.hl7.fhir.exceptions.FHIRException;
021import org.hl7.fhir.exceptions.FHIRFormatError;
022import org.hl7.fhir.r5.context.IWorkerContext;
023import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
024import org.hl7.fhir.r5.model.Address;
025import org.hl7.fhir.r5.model.Annotation;
026import org.hl7.fhir.r5.model.BackboneType;
027import org.hl7.fhir.r5.model.Base;
028import org.hl7.fhir.r5.model.Base64BinaryType;
029import org.hl7.fhir.r5.model.BaseDateTimeType;
030import org.hl7.fhir.r5.model.CanonicalResource;
031import org.hl7.fhir.r5.model.CanonicalType;
032import org.hl7.fhir.r5.model.CodeSystem;
033import org.hl7.fhir.r5.model.CodeableConcept;
034import org.hl7.fhir.r5.model.CodeableReference;
035import org.hl7.fhir.r5.model.Coding;
036import org.hl7.fhir.r5.model.ContactPoint;
037import org.hl7.fhir.r5.model.ContactPoint.ContactPointSystem;
038import org.hl7.fhir.r5.model.DataRequirement;
039import org.hl7.fhir.r5.model.DataRequirement.DataRequirementCodeFilterComponent;
040import org.hl7.fhir.r5.model.DataRequirement.DataRequirementDateFilterComponent;
041import org.hl7.fhir.r5.model.DataRequirement.DataRequirementSortComponent;
042import org.hl7.fhir.r5.model.DataRequirement.SortDirection;
043import org.hl7.fhir.r5.model.DataType;
044import org.hl7.fhir.r5.model.DateTimeType;
045import org.hl7.fhir.r5.model.DateType;
046import org.hl7.fhir.r5.model.ElementDefinition;
047import org.hl7.fhir.r5.model.Enumeration;
048import org.hl7.fhir.r5.model.Expression;
049import org.hl7.fhir.r5.model.Extension;
050import org.hl7.fhir.r5.model.HumanName;
051import org.hl7.fhir.r5.model.HumanName.NameUse;
052import org.hl7.fhir.r5.model.IdType;
053import org.hl7.fhir.r5.model.Identifier;
054import org.hl7.fhir.r5.model.MarkdownType;
055import org.hl7.fhir.r5.model.Money;
056import org.hl7.fhir.r5.model.Period;
057import org.hl7.fhir.r5.model.PrimitiveType;
058import org.hl7.fhir.r5.model.Quantity;
059import org.hl7.fhir.r5.model.Range;
060import org.hl7.fhir.r5.model.Reference;
061import org.hl7.fhir.r5.model.Resource;
062import org.hl7.fhir.r5.model.SampledData;
063import org.hl7.fhir.r5.model.StringType;
064import org.hl7.fhir.r5.model.StructureDefinition;
065import org.hl7.fhir.r5.model.Timing;
066import org.hl7.fhir.r5.model.Timing.EventTiming;
067import org.hl7.fhir.r5.model.Timing.TimingRepeatComponent;
068import org.hl7.fhir.r5.model.Timing.UnitsOfTime;
069import org.hl7.fhir.r5.model.UriType;
070import org.hl7.fhir.r5.model.UsageContext;
071import org.hl7.fhir.r5.model.ValueSet;
072import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
073import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent;
074import org.hl7.fhir.r5.renderers.utils.BaseWrappers.BaseWrapper;
075import org.hl7.fhir.r5.renderers.CodeResolver.CodeResolution;
076import org.hl7.fhir.r5.renderers.utils.RenderingContext;
077import org.hl7.fhir.r5.renderers.utils.RenderingContext.GenerationRules;
078import org.hl7.fhir.r5.renderers.utils.RenderingContext.ResourceRendererMode;
079import org.hl7.fhir.r5.terminologies.JurisdictionUtilities;
080import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
081import org.hl7.fhir.utilities.Utilities;
082import org.hl7.fhir.utilities.VersionUtilities;
083import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
084import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece;
085import org.hl7.fhir.utilities.xhtml.NodeType;
086import org.hl7.fhir.utilities.xhtml.XhtmlNode;
087import org.hl7.fhir.utilities.xhtml.XhtmlParser;
088
089import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
090
091public class DataRenderer extends Renderer implements CodeResolver {
092  
093  // -- 1. context --------------------------------------------------------------
094    
095  public DataRenderer(RenderingContext context) {
096    super(context);
097  }
098
099  public DataRenderer(IWorkerContext worker) {
100    super(worker);
101  }
102
103  // -- 2. Markdown support -------------------------------------------------------
104   
105  public static String processRelativeUrls(String markdown, String path) {
106    if (markdown == null) {
107      return "";
108    }
109    if (!Utilities.isAbsoluteUrl(path)) {
110      return markdown;
111    }
112    String basePath = path.contains("/") ? path.substring(0, path.lastIndexOf("/")+1) : path+"/";
113    StringBuilder b = new StringBuilder();
114    int i = 0;
115    while (i < markdown.length()) {
116      if (i < markdown.length()-3 && markdown.substring(i, i+2).equals("](")) {
117        int j = i + 2;
118        while (j < markdown.length() && markdown.charAt(j) != ')')
119          j++;
120        if (j < markdown.length()) {
121          String url = markdown.substring(i+2, j);
122          if (!Utilities.isAbsoluteUrl(url) && !url.startsWith("..")) {
123            // it's relative - so it's relative to the base URL
124              b.append("](");
125              b.append(basePath);
126          } else {
127            b.append("](");
128          }
129          i = i + 1;
130        } else 
131          b.append(markdown.charAt(i));
132      } else {
133        b.append(markdown.charAt(i));
134      }
135      i++;
136    }
137    return b.toString();
138  }
139
140  protected void addMarkdown(XhtmlNode x, String text, String path) throws FHIRFormatError, IOException, DefinitionException {
141    addMarkdown(x, processRelativeUrls(text, path));
142  }
143  
144  protected void addMarkdown(XhtmlNode x, String text) throws FHIRFormatError, IOException, DefinitionException {
145    if (text != null) {
146      // 1. custom FHIR extensions
147      while (text.contains("[[[")) {
148        String left = text.substring(0, text.indexOf("[[["));
149        String link = text.substring(text.indexOf("[[[")+3, text.indexOf("]]]"));
150        String right = text.substring(text.indexOf("]]]")+3);
151        String path = null;
152        String url = link;
153        String[] parts = link.split("\\#");
154        if (parts[0].contains(".")) {
155          path = parts[0];
156          parts[0] = parts[0].substring(0, parts[0].indexOf("."));
157        }
158        StructureDefinition p = getContext().getWorker().fetchResource(StructureDefinition.class, parts[0]);
159        if (p == null)
160          p = getContext().getWorker().fetchTypeDefinition(parts[0]);
161        if (p == null)
162          p = getContext().getWorker().fetchResource(StructureDefinition.class, link);
163        if (p != null) {
164          url = p.getWebPath();
165          if (url == null)
166            url = p.getUserString("filename");
167        } else
168          throw new DefinitionException("Unable to resolve markdown link "+link);
169  
170        text = left+"["+link+"]("+url+(path == null ? "" : "#"+path)+")"+right;
171      }
172  
173      // 2. markdown
174      String s = getContext().getMarkdown().process(text, "narrative generator");
175      XhtmlParser p = new XhtmlParser();
176      XhtmlNode m;
177      try {
178        m = p.parse("<div>"+s+"</div>", "div");
179      } catch (org.hl7.fhir.exceptions.FHIRFormatError e) {
180        throw new FHIRFormatError(e.getMessage(), e);
181      }
182      x.getChildNodes().addAll(m.getChildNodes());
183    }
184  }
185
186  protected void smartAddText(XhtmlNode p, String text) {
187    if (text == null)
188      return;
189  
190    String[] lines = text.split("\\r\\n");
191    for (int i = 0; i < lines.length; i++) {
192      if (i > 0)
193        p.br();
194      p.addText(lines[i]);
195    }
196  }
197 
198  // -- 3. General Purpose Terminology Support -----------------------------------------
199
200  private static String month(String m) {
201    switch (m) {
202    case "1" : return "Jan";
203    case "2" : return "Feb";
204    case "3" : return "Mar";
205    case "4" : return "Apr";
206    case "5" : return "May";
207    case "6" : return "Jun";
208    case "7" : return "Jul";
209    case "8" : return "Aug";
210    case "9" : return "Sep";
211    case "10" : return "Oct";
212    case "11" : return "Nov";
213    case "12" : return "Dec";
214    default: return null;
215    }
216  }
217  
218  public static String describeVersion(String version) {
219    if (version.startsWith("http://snomed.info/sct")) {
220      String[] p = version.split("\\/");
221      String ed = null;
222      String dt = "";
223
224      if (p[p.length-2].equals("version")) {
225        ed = p[p.length-3];
226        String y = p[p.length-3].substring(4, 8);
227        String m = p[p.length-3].substring(2, 4); 
228        dt = " rel. "+month(m)+" "+y;
229      } else {
230        ed = p[p.length-1];
231      }
232      switch (ed) {
233      case "900000000000207008": return "Intl"+dt; 
234      case "731000124108": return "US"+dt; 
235      case "32506021000036107": return "AU"+dt; 
236      case "449081005": return "ES"+dt; 
237      case "554471000005108": return "DK"+dt; 
238      case "11000146104": return "NL"+dt; 
239      case "45991000052106": return "SE"+dt; 
240      case "999000041000000102": return "UK"+dt; 
241      case "20611000087101": return "CA"+dt; 
242      case "11000172109": return "BE"+dt; 
243      default: return "??"+dt; 
244      }      
245    } else {
246      return version;
247    }
248  }
249  
250  public static String describeSystem(String system) {
251    if (system == null)
252      return "[not stated]";
253    if (system.equals("http://loinc.org"))
254      return "LOINC";
255    if (system.startsWith("http://snomed.info"))
256      return "SNOMED CT";
257    if (system.equals("http://www.nlm.nih.gov/research/umls/rxnorm"))
258      return "RxNorm";
259    if (system.equals("http://hl7.org/fhir/sid/icd-9"))
260      return "ICD-9";
261    if (system.equals("http://dicom.nema.org/resources/ontology/DCM"))
262      return "DICOM";
263    if (system.equals("http://unitsofmeasure.org"))
264      return "UCUM";
265  
266    return system;
267  }
268
269  public String displaySystem(String system) {
270    if (system == null)
271      return "[not stated]";
272    if (system.equals("http://loinc.org"))
273      return "LOINC";
274    if (system.startsWith("http://snomed.info"))
275      return "SNOMED CT";
276    if (system.equals("http://www.nlm.nih.gov/research/umls/rxnorm"))
277      return "RxNorm";
278    if (system.equals("http://hl7.org/fhir/sid/icd-9"))
279      return "ICD-9";
280    if (system.equals("http://dicom.nema.org/resources/ontology/DCM"))
281      return "DICOM";
282    if (system.equals("http://unitsofmeasure.org"))
283      return "UCUM";
284
285    CodeSystem cs = context.getContext().fetchCodeSystem(system);
286    if (cs != null) {
287      return cs.present();
288    }
289    return tails(system);
290  }
291
292  private String tails(String system) {
293    if (system.contains("/")) {
294      return system.substring(system.lastIndexOf("/")+1);
295    } else {
296      return "unknown";
297    }
298  }
299
300  protected String makeAnchor(String codeSystem, String code) {
301    String s = codeSystem+'-'+code;
302    StringBuilder b = new StringBuilder();
303    for (char c : s.toCharArray()) {
304      if (Character.isAlphabetic(c) || Character.isDigit(c) || c == '.')
305        b.append(c);
306      else
307        b.append('-');
308    }
309    return b.toString();
310  }
311
312  private String lookupCode(String system, String version, String code) {
313    if (JurisdictionUtilities.isJurisdiction(system)) {
314      return JurisdictionUtilities.displayJurisdiction(system+"#"+code);
315    }
316    ValidationResult t = getContext().getWorker().validateCode(getContext().getTerminologyServiceOptions().withVersionFlexible(true), system, version, code, null);
317
318    if (t != null && t.getDisplay() != null)
319      return t.getDisplay();
320    else
321      return code;
322  }
323
324  protected String describeLang(String lang) {
325    // special cases:
326    if ("fr-CA".equals(lang)) {
327      return "French (Canadian)"; // this one was omitted from the value set
328    }
329    ValueSet v = getContext().getWorker().fetchResource(ValueSet.class, "http://hl7.org/fhir/ValueSet/languages");
330    if (v != null) {
331      ConceptReferenceComponent l = null;
332      for (ConceptReferenceComponent cc : v.getCompose().getIncludeFirstRep().getConcept()) {
333        if (cc.getCode().equals(lang))
334          l = cc;
335      }
336      if (l == null) {
337        if (lang.contains("-")) {
338          lang = lang.substring(0, lang.indexOf("-"));
339        }
340        for (ConceptReferenceComponent cc : v.getCompose().getIncludeFirstRep().getConcept()) {
341          if (cc.getCode().equals(lang)) {
342            l = cc;
343            break;
344          }
345        }
346        if (l == null) {
347          for (ConceptReferenceComponent cc : v.getCompose().getIncludeFirstRep().getConcept()) {
348            if (cc.getCode().startsWith(lang+"-")) {
349              l = cc;
350              break;
351            }
352          }
353        }
354      }
355      if (l != null) {
356        if (lang.contains("-"))
357          lang = lang.substring(0, lang.indexOf("-"));
358        String en = l.getDisplay();
359        String nativelang = null;
360        for (ConceptReferenceDesignationComponent cd : l.getDesignation()) {
361          if (cd.getLanguage().equals(lang))
362            nativelang = cd.getValue();
363        }
364        if (nativelang == null)
365          return en+" ("+lang+")";
366        else
367          return nativelang+" ("+en+", "+lang+")";
368      }
369    }
370    return lang;
371  }
372
373  private boolean isCanonical(String path) {
374    if (!path.endsWith(".url")) 
375      return false;
376    String t = path.substring(0, path.length()-4);
377    StructureDefinition sd = getContext().getWorker().fetchTypeDefinition(t);
378    if (sd == null)
379      return false;
380    if (VersionUtilities.getCanonicalResourceNames(getContext().getWorker().getVersion()).contains(t)) {
381      return true;
382    }
383    if (Utilities.existsInList(t, 
384        "ActivityDefinition", "CapabilityStatement", "ChargeItemDefinition", "Citation", "CodeSystem",
385        "CompartmentDefinition", "ConceptMap", "ConditionDefinition", "EventDefinition", "Evidence", "EvidenceReport", "EvidenceVariable",
386        "ExampleScenario", "GraphDefinition", "ImplementationGuide", "Library", "Measure", "MessageDefinition", "NamingSystem", "PlanDefinition"
387        ))
388      return true;
389    return false;
390  }
391
392  // -- 4. Language support ------------------------------------------------------
393  
394  protected String translate(String source, String content) {
395    return content;
396  }
397
398  public String gt(@SuppressWarnings("rawtypes") PrimitiveType value) {
399    return value.primitiveValue();
400  }
401  
402  // -- 6. General purpose extension rendering ---------------------------------------------- 
403
404  public boolean hasRenderableExtensions(DataType element) {
405    for (Extension ext : element.getExtension()) {
406      if (canRender(ext)) {
407        return true;
408      }
409    }
410    return false;
411  }
412  
413  public boolean hasRenderableExtensions(BackboneType element) {
414    for (Extension ext : element.getExtension()) {
415      if (canRender(ext)) {
416        return true;
417      }
418    }
419    return element.hasModifierExtension();  
420  }
421  
422  private String getExtensionLabel(Extension ext) {
423    StructureDefinition sd = context.getWorker().fetchResource(StructureDefinition.class, ext.getUrl());
424    if (sd != null && ext.hasValue() && ext.getValue().isPrimitive() && sd.hasSnapshot()) {
425      for (ElementDefinition ed : sd.getSnapshot().getElement()) {
426        if (Utilities.existsInList(ed.getPath(), "Extension", "Extension.value[x]") && ed.hasLabel()) {
427          return ed.getLabel();
428        }
429      }
430    }
431    return null;    
432  }
433  
434  private boolean canRender(Extension ext) {
435    return getExtensionLabel(ext) != null;
436  }
437
438  public void renderExtensionsInList(XhtmlNode ul, DataType element) throws FHIRFormatError, DefinitionException, IOException {
439    for (Extension ext : element.getExtension()) {
440      if (canRender(ext)) {
441        String lbl = getExtensionLabel(ext);
442        XhtmlNode li = ul.li();
443        li.tx(lbl);
444        li.tx(": ");
445        render(li, ext.getValue());
446      }
447    }
448  }
449  
450  public void renderExtensionsInList(XhtmlNode ul, BackboneType element) throws FHIRFormatError, DefinitionException, IOException {
451    for (Extension ext : element.getModifierExtension()) {
452      if (canRender(ext)) {
453        String lbl = getExtensionLabel(ext);
454        XhtmlNode li = ul.li();
455        li = li.b();
456        li.tx(lbl);
457        li.tx(": ");        
458        render(li, ext.getValue());
459      } else {
460        // somehow have to do better than this 
461        XhtmlNode li = ul.li();
462        li.b().tx("WARNING: Unrenderable Modifier Extension!");
463      }
464    }
465    for (Extension ext : element.getExtension()) {
466      if (canRender(ext)) {
467        String lbl = getExtensionLabel(ext);
468        XhtmlNode li = ul.li();
469        li.tx(lbl);
470        li.tx(": ");
471        render(li, ext.getValue());
472      }
473    }
474  }
475  
476  public void renderExtensionsInText(XhtmlNode div, DataType element, String sep) throws FHIRFormatError, DefinitionException, IOException {
477    boolean first = true;
478    for (Extension ext : element.getExtension()) {
479      if (canRender(ext)) {
480        if (first) {
481          first = false;
482        } else {
483          div.tx(sep);
484          div.tx(" ");
485        }
486         
487        String lbl = getExtensionLabel(ext);
488        div.tx(lbl);
489        div.tx(": ");
490        render(div, ext.getValue());
491      }
492    }
493  }
494  
495  public void renderExtensionsInList(XhtmlNode div, BackboneType element, String sep) throws FHIRFormatError, DefinitionException, IOException {
496    boolean first = true;
497    for (Extension ext : element.getModifierExtension()) {
498      if (first) {
499        first = false;
500      } else {
501        div.tx(sep);
502        div.tx(" ");
503      }
504      if (canRender(ext)) {
505        String lbl = getExtensionLabel(ext);
506        XhtmlNode b = div.b();
507        b.tx(lbl);
508        b.tx(": ");
509        render(div, ext.getValue());
510      } else {
511        // somehow have to do better than this 
512        div.b().tx("WARNING: Unrenderable Modifier Extension!");
513      }
514    }
515    for (Extension ext : element.getExtension()) {
516      if (canRender(ext)) {
517        if (first) {
518          first = false;
519        } else {
520          div.tx(sep);
521          div.tx(" ");
522        }
523         
524        String lbl = getExtensionLabel(ext);
525        div.tx(lbl);
526        div.tx(": ");
527        render(div, ext.getValue());
528      }
529    }
530
531  }
532  
533  // -- 6. Data type Rendering ---------------------------------------------- 
534
535  public static String display(IWorkerContext context, DataType type) {
536    return new DataRenderer(new RenderingContext(context, null, null, "http://hl7.org/fhir/R4", "", null, ResourceRendererMode.END_USER, GenerationRules.VALID_RESOURCE)).display(type);
537  }
538  
539  public String displayBase(Base b) {
540    if (b instanceof DataType) {
541      return display((DataType) b);
542    } else {
543      return "No display for "+b.fhirType();      
544    }
545  }
546  
547  public String display(DataType type) {
548    if (type == null || type.isEmpty()) {
549      return "";
550    }
551    
552    if (type instanceof Coding) {
553      return displayCoding((Coding) type);
554    } else if (type instanceof CodeableConcept) {
555      return displayCodeableConcept((CodeableConcept) type);
556    } else if (type instanceof Identifier) {
557      return displayIdentifier((Identifier) type);
558    } else if (type instanceof HumanName) {
559      return displayHumanName((HumanName) type);
560    } else if (type instanceof Address) {
561      return displayAddress((Address) type);
562    } else if (type instanceof ContactPoint) {
563      return displayContactPoint((ContactPoint) type);
564    } else if (type instanceof Quantity) {
565      return displayQuantity((Quantity) type);
566    } else if (type instanceof Range) {
567      return displayRange((Range) type);
568    } else if (type instanceof Period) {
569      return displayPeriod((Period) type);
570    } else if (type instanceof Timing) {
571      return displayTiming((Timing) type);
572    } else if (type instanceof SampledData) {
573      return displaySampledData((SampledData) type);
574    } else if (type.isDateTime()) {
575      return displayDateTime((BaseDateTimeType) type);
576    } else if (type.isPrimitive()) {
577      return type.primitiveValue();
578    } else {
579      return "No display for "+type.fhirType();
580    }
581  }
582
583  private String displayDateTime(BaseDateTimeType type) {
584    if (!type.hasPrimitiveValue()) {
585      return "";
586    }
587    
588    // relevant inputs in rendering context:
589    // timeZone, dateTimeFormat, locale, mode
590    //   timezone - application specified timezone to use. 
591    //        null = default to the time of the date/time itself
592    //   dateTimeFormat - application specified format for date times
593    //        null = default to ... depends on mode
594    //   mode - if rendering mode is technical, format defaults to XML format
595    //   locale - otherwise, format defaults to SHORT for the Locale (which defaults to default Locale)  
596    if (isOnlyDate(type.getPrecision())) {
597      
598      DateTimeFormatter fmt = getDateFormatForPrecision(type);      
599      LocalDate date = LocalDate.of(type.getYear(), type.getMonth()+1, type.getDay());
600      return fmt.format(date);
601    }
602
603    DateTimeFormatter fmt = context.getDateTimeFormat();
604    if (fmt == null) {
605      if (context.isTechnicalMode()) {
606        fmt = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
607      } else {
608        fmt = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withLocale(context.getLocale());
609      }
610    }
611    ZonedDateTime zdt = ZonedDateTime.parse(type.primitiveValue());
612    ZoneId zone = context.getTimeZoneId();
613    if (zone != null) {
614      zdt = zdt.withZoneSameInstant(zone);
615    }
616    return fmt.format(zdt);
617  }
618
619  private DateTimeFormatter getDateFormatForPrecision(BaseDateTimeType type) {
620    DateTimeFormatter fmt = getContextDateFormat(type);
621    if (fmt != null) {
622      return fmt;
623    }
624    if (context.isTechnicalMode()) {
625      switch (type.getPrecision()) {
626      case YEAR:
627        return new DateTimeFormatterBuilder().appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD).toFormatter();
628      case MONTH:
629        return  new DateTimeFormatterBuilder().appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD).appendLiteral('-').appendValue(MONTH_OF_YEAR, 2).toFormatter();
630      default:
631        return DateTimeFormatter.ISO_DATE;
632      }
633    } else {
634      switch (type.getPrecision()) {
635      case YEAR:
636        return DateTimeFormatter.ofPattern("uuuu");
637      case MONTH:
638        return DateTimeFormatter.ofPattern("MMM uuuu");
639      default:
640        return DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(context.getLocale());
641      }
642    }
643  }
644
645  private DateTimeFormatter getContextDateFormat(BaseDateTimeType type) {
646    switch (type.getPrecision()) {
647    case YEAR:
648      return context.getDateYearFormat();
649    case MONTH:
650      return context.getDateYearMonthFormat();
651    default:
652      return context.getDateFormat();
653    }
654  }   
655  
656  private boolean isOnlyDate(TemporalPrecisionEnum temporalPrecisionEnum) {
657    return temporalPrecisionEnum == TemporalPrecisionEnum.YEAR || temporalPrecisionEnum == TemporalPrecisionEnum.MONTH || temporalPrecisionEnum == TemporalPrecisionEnum.DAY;
658  }
659
660  public String display(BaseWrapper type) {
661    return "to do";   
662  }
663
664  public void render(XhtmlNode x, BaseWrapper type) throws FHIRFormatError, DefinitionException, IOException  {
665    Base base = null;
666    try {
667      base = type.getBase();
668    } catch (FHIRException | IOException e) {
669      x.tx("Error: " + e.getMessage()); // this shouldn't happen - it's an error in the library itself
670      return;
671    }
672    if (base instanceof DataType) {
673      render(x, (DataType) base);
674    } else {
675      x.tx("to do: "+base.fhirType());
676    }
677  }
678  
679  public void renderBase(XhtmlNode x, Base b) throws FHIRFormatError, DefinitionException, IOException {
680    if (b instanceof DataType) {
681      render(x, (DataType) b);
682    } else {
683      x.tx("No display for "+b.fhirType());      
684    }
685  }
686  
687  public void render(XhtmlNode x, DataType type) throws FHIRFormatError, DefinitionException, IOException {
688    if (type instanceof BaseDateTimeType) {
689      x.tx(displayDateTime((BaseDateTimeType) type));
690    } else if (type instanceof UriType) {
691      renderUri(x, (UriType) type);
692    } else if (type instanceof Annotation) {
693      renderAnnotation(x, (Annotation) type);
694    } else if (type instanceof Coding) {
695      renderCodingWithDetails(x, (Coding) type);
696    } else if (type instanceof CodeableConcept) {
697      renderCodeableConcept(x, (CodeableConcept) type);
698    } else if (type instanceof Identifier) {
699      renderIdentifier(x, (Identifier) type);
700    } else if (type instanceof HumanName) {
701      renderHumanName(x, (HumanName) type);
702    } else if (type instanceof Address) {
703      renderAddress(x, (Address) type);
704    } else if (type instanceof Expression) {
705      renderExpression(x, (Expression) type);
706    } else if (type instanceof Money) {
707      renderMoney(x, (Money) type);
708    } else if (type instanceof ContactPoint) {
709      renderContactPoint(x, (ContactPoint) type);
710    } else if (type instanceof Quantity) {
711      renderQuantity(x, (Quantity) type);
712    } else if (type instanceof Range) {
713      renderRange(x, (Range) type);
714    } else if (type instanceof Period) {
715      renderPeriod(x, (Period) type);
716    } else if (type instanceof Timing) {
717      renderTiming(x, (Timing) type);
718    } else if (type instanceof SampledData) {
719      renderSampledData(x, (SampledData) type);
720    } else if (type instanceof Reference) {
721      renderReference(x, (Reference) type);
722    } else if (type instanceof CodeableReference) {
723      CodeableReference cr = (CodeableReference) type;
724      if (cr.hasConcept()) {
725        renderCodeableConcept(x, cr.getConcept());
726      } else { 
727        renderReference(x, cr.getReference());
728      }
729    } else if (type instanceof MarkdownType) {
730      addMarkdown(x, ((MarkdownType) type).asStringValue());
731    } else if (type instanceof Base64BinaryType) {
732      Base64BinaryType b64 = (Base64BinaryType) type;
733      x.tx("(base64 data - "+(b64.getValue() == null ? "0" : b64.getValue().length)+" bytes)");
734    } else if (type.isPrimitive()) {
735      x.tx(type.primitiveValue());
736    } else {
737      x.tx("No display for "+type.fhirType());      
738    }
739  }
740
741  private void renderReference(XhtmlNode x, Reference ref) {
742     if (ref.hasDisplay()) {
743       x.tx(ref.getDisplay());
744     } else if (ref.hasReference()) {
745       x.tx(ref.getReference());
746     } else {
747       x.tx("??");
748     }
749  }
750
751  public void renderDateTime(XhtmlNode x, Base e) {
752    if (e.hasPrimitiveValue()) {
753      x.addText(displayDateTime((DateTimeType) e));
754    }
755  }
756
757  public void renderDate(XhtmlNode x, Base e) {
758    if (e.hasPrimitiveValue()) {
759      x.addText(displayDateTime((DateType) e));
760    }
761  }
762
763  public void renderDateTime(XhtmlNode x, String s) {
764    if (s != null) {
765      DateTimeType dt = new DateTimeType(s);
766      x.addText(displayDateTime(dt));
767    }
768  }
769
770  protected void renderUri(XhtmlNode x, UriType uri) {
771    if (uri.getValue().startsWith("mailto:")) {
772      x.ah(uri.getValue()).addText(uri.getValue().substring(7));
773    } else {
774      Resource r = context.getContext().fetchResource(Resource.class, uri.getValue());
775      if (r != null && r.getWebPath() != null) {
776        if (r instanceof CanonicalResource) {
777          x.ah(r.getWebPath()).addText(((CanonicalResource) r).present());          
778        } else {
779          x.ah(r.getWebPath()).addText(uri.getValue());          
780        }
781      } else if (Utilities.isAbsoluteUrlLinkable(uri.getValue()) && !(uri instanceof IdType)) {
782        x.ah(uri.getValue()).addText(uri.getValue());
783      } else {
784        x.addText(uri.getValue());
785      }
786    }
787  }
788  
789  protected void renderUri(XhtmlNode x, UriType uri, String path, String id, Resource src) {
790    if (isCanonical(path)) {
791      x.code().tx(uri.getValue());
792    } else {
793      String url = uri.getValue();
794      if (url == null) {
795        x.b().tx(uri.getValue());
796      } else if (uri.getValue().startsWith("mailto:")) {
797        x.ah(uri.getValue()).addText(uri.getValue().substring(7));
798      } else {
799        Resource target = context.getContext().fetchResource(Resource.class, uri.getValue(), src);
800        if (target != null && target.hasWebPath()) {
801          String title = target instanceof CanonicalResource ? ((CanonicalResource) target).present() : uri.getValue();
802          x.ah(target.getWebPath()).addText(title);
803        } else if (uri.getValue().contains("|")) {
804          x.ah(uri.getValue().substring(0, uri.getValue().indexOf("|"))).addText(uri.getValue());
805        } else if (url.startsWith("http:") || url.startsWith("https:") || url.startsWith("ftp:")) {
806          x.ah(uri.getValue()).addText(uri.getValue());        
807        } else {
808          x.code().addText(uri.getValue());        
809        }
810      }
811    }
812  }
813
814  protected void renderAnnotation(XhtmlNode x, Annotation annot) {
815    renderAnnotation(x, annot, false);
816  }
817
818  protected void renderAnnotation(XhtmlNode x, Annotation a, boolean showCodeDetails) throws FHIRException {
819    StringBuilder b = new StringBuilder();
820    if (a.hasText()) {
821      b.append(a.getText());
822    }
823
824    if (a.hasText() && (a.hasAuthor() || a.hasTimeElement())) {
825      b.append(" (");
826    }
827
828    if (a.hasAuthor()) {
829      b.append("By ");
830      if (a.hasAuthorReference()) {
831        b.append(a.getAuthorReference().getReference());
832      } else if (a.hasAuthorStringType()) {
833        b.append(a.getAuthorStringType().getValue());
834      }
835    }
836
837
838    if (a.hasTimeElement()) {
839      if (b.length() > 0) {
840        b.append(" ");
841      }
842      b.append("@").append(a.getTimeElement().toHumanDisplay());
843    }
844    if (a.hasText() && (a.hasAuthor() || a.hasTimeElement())) {
845      b.append(")");
846    }
847
848
849    x.addText(b.toString());
850  }
851
852  public String displayCoding(Coding c) {
853    String s = "";
854    if (context.isTechnicalMode()) {
855      s = c.getDisplay();
856      if (Utilities.noString(s)) {
857        s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());        
858      }
859      if (Utilities.noString(s)) {
860        s = displayCodeTriple(c.getSystem(), c.getVersion(), c.getCode());
861      } else if (c.hasSystem()) {
862        s = s + " ("+displayCodeTriple(c.getSystem(), c.getVersion(), c.getCode())+")";
863      } else if (c.hasCode()) {
864        s = s + " ("+c.getCode()+")";
865      }
866    } else {
867    if (c.hasDisplayElement())
868      return c.getDisplay();
869    if (Utilities.noString(s))
870      s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
871    if (Utilities.noString(s))
872      s = c.getCode();
873    }
874    return s;
875  }
876
877  private String displayCodeSource(String system, String version) {
878    String s = displaySystem(system);
879    if (version != null) {
880      s = s + "["+describeVersion(version)+"]";
881    }
882    return s;    
883  }
884  
885  private String displayCodeTriple(String system, String version, String code) {
886    if (system == null) {
887      if (code == null) {
888        return "";
889      } else {
890        return "#"+code;
891      }
892    } else {
893      String s = displayCodeSource(system, version);
894      if (code != null) {
895        s = s + "#"+code;
896      }
897      return s;
898    }
899  }
900
901  public String displayCoding(List<Coding> list) {
902    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
903    for (Coding c : list) {
904      b.append(displayCoding(c));
905    }
906    return b.toString();
907  }
908
909  protected void renderCoding(XhtmlNode x, Coding c) {
910    renderCoding(x, c, false);
911  }
912  
913  protected void renderCoding(HierarchicalTableGenerator gen, List<Piece> pieces, Coding c) {
914    if (c.isEmpty()) {
915      return;
916    }
917
918    String url = getLinkForSystem(c.getSystem(), c.getVersion());
919    String name = displayCodeSource(c.getSystem(), c.getVersion());
920    if (!Utilities.noString(url)) {
921      pieces.add(gen.new Piece(url, name, c.getSystem()+(c.hasVersion() ? "#"+c.getVersion() : "")));
922    } else { 
923      pieces.add(gen.new Piece(null, name, c.getSystem()+(c.hasVersion() ? "#"+c.getVersion() : "")));
924    }
925    pieces.add(gen.new Piece(null, "#"+c.getCode(), null));
926    String s = c.getDisplay();
927    if (Utilities.noString(s)) {
928      s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
929    }
930    if (!Utilities.noString(s)) {
931      pieces.add(gen.new Piece(null, " \""+s+"\"", null));
932    }
933  }
934  
935  private String getLinkForSystem(String system, String version) {
936    if ("http://snomed.info/sct".equals(system)) {
937      return "https://browser.ihtsdotools.org/";      
938    } else if ("http://loinc.org".equals(system)) {
939      return "https://loinc.org/";            
940    } else if ("http://unitsofmeasure.org".equals(system)) {
941      return "http://ucum.org";            
942    } else {
943      String url = system;
944      if (version != null) {
945        url = url + "|"+version;
946      }
947      CodeSystem cs = context.getWorker().fetchCodeSystem(url);
948      if (cs != null && cs.hasWebPath()) {
949        return cs.getWebPath();
950      }
951      return null;
952    }
953  }
954  
955  protected String getLinkForCode(String system, String version, String code) {
956    if ("http://snomed.info/sct".equals(system)) {
957      if (!Utilities.noString(code)) {
958        return "http://snomed.info/id/"+code;        
959      } else {
960        return "https://browser.ihtsdotools.org/";
961      }
962    } else if ("http://loinc.org".equals(system)) {
963      if (!Utilities.noString(code)) {
964        return "https://loinc.org/"+code;
965      } else {
966        return "https://loinc.org/";
967      }
968    } else if ("http://www.nlm.nih.gov/research/umls/rxnorm".equals(system)) {
969      if (!Utilities.noString(code)) {
970        return "https://mor.nlm.nih.gov/RxNav/search?searchBy=RXCUI&searchTerm="+code;        
971      } else {
972        return "https://www.nlm.nih.gov/research/umls/rxnorm/index.html";
973      }
974    } else if ("urn:iso:std:iso:3166".equals(system)) {
975      if (!Utilities.noString(code)) {
976        return "https://en.wikipedia.org/wiki/ISO_3166-2:"+code;        
977      } else {
978        return "https://en.wikipedia.org/wiki/ISO_3166-2";
979      }
980    } else {
981      CodeSystem cs = context.getWorker().fetchCodeSystem(system, version);
982      if (cs != null && cs.hasWebPath()) {
983        if (!Utilities.noString(code)) {
984          return cs.getWebPath()+"#"+cs.getId()+"-"+Utilities.nmtokenize(code);
985        } else {
986          return cs.getWebPath();
987        }
988      }
989    }  
990    return null;
991  }
992  
993  public CodeResolution resolveCode(String system, String code) {
994    return resolveCode(new Coding().setSystem(system).setCode(code));
995  }
996
997  public CodeResolution resolveCode(Coding c) {
998    String systemName;
999    String systemLink;
1000    String link;
1001    String display = null;
1002    String hint;
1003    
1004    if (c.hasDisplayElement())
1005      display = c.getDisplay();
1006    if (Utilities.noString(display))
1007      display = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
1008    if (Utilities.noString(display)) {
1009      display = c.getCode();
1010    }
1011    
1012    CodeSystem cs = context.getWorker().fetchCodeSystem(c.getSystem());
1013    systemLink = cs != null ? cs.getWebPath() : null;
1014    systemName = cs != null ? cs.present() : describeSystem(c.getSystem());
1015    link = getLinkForCode(c.getSystem(), c.getVersion(), c.getCode());
1016
1017    hint = systemName+": "+display+(c.hasVersion() ? " (version = "+c.getVersion()+")" : "");
1018    return new CodeResolution(systemName, systemLink, link, display, hint);
1019  }
1020  
1021  public CodeResolution resolveCode(CodeableConcept code) {
1022    if (code.hasCoding()) {
1023      return resolveCode(code.getCodingFirstRep());
1024    } else {
1025      return new CodeResolution(null, null, null, code.getText(), code.getText());
1026    }
1027  }
1028  protected void renderCodingWithDetails(XhtmlNode x, Coding c) {
1029    String s = "";
1030    if (c.hasDisplayElement())
1031      s = c.getDisplay();
1032    if (Utilities.noString(s))
1033      s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
1034
1035    CodeSystem cs = context.getWorker().fetchCodeSystem(c.getSystem());
1036
1037    String sn = cs != null ? cs.present() : describeSystem(c.getSystem());
1038    String link = getLinkForCode(c.getSystem(), c.getVersion(), c.getCode());
1039    if (link != null) {
1040      x.ah(link).tx(sn);
1041    } else {
1042      x.tx(sn);
1043    }
1044    
1045    x.tx(" ");
1046    x.tx(c.getCode());
1047    if (!Utilities.noString(s)) {
1048      x.tx(": ");
1049      x.tx(s);
1050    }
1051    if (c.hasVersion()) {
1052      x.tx(" (version = "+c.getVersion()+")");
1053    }
1054  }
1055  
1056  protected void renderCoding(XhtmlNode x, Coding c, boolean showCodeDetails) {
1057    String s = "";
1058    if (c.hasDisplayElement())
1059      s = c.getDisplay();
1060    if (Utilities.noString(s))
1061      s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
1062
1063    if (Utilities.noString(s))
1064      s = c.getCode();
1065
1066    if (showCodeDetails) {
1067      x.addText(s+" (Details: "+TerminologyRenderer.describeSystem(c.getSystem())+" code "+c.getCode()+" = '"+lookupCode(c.getSystem(), c.getVersion(), c.getCode())+"', stated as '"+c.getDisplay()+"')");
1068    } else
1069      x.span(null, "{"+c.getSystem()+" "+c.getCode()+"}").addText(s);
1070  }
1071
1072  public String displayCodeableConcept(CodeableConcept cc) {
1073    String s = cc.getText();
1074    if (Utilities.noString(s)) {
1075      for (Coding c : cc.getCoding()) {
1076        if (c.hasDisplayElement()) {
1077          s = c.getDisplay();
1078          break;
1079        }
1080      }
1081    }
1082    if (Utilities.noString(s)) {
1083      // still? ok, let's try looking it up
1084      for (Coding c : cc.getCoding()) {
1085        if (c.hasCode() && c.hasSystem()) {
1086          s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
1087          if (!Utilities.noString(s))
1088            break;
1089        }
1090      }
1091    }
1092
1093    if (Utilities.noString(s)) {
1094      if (cc.getCoding().isEmpty())
1095        s = "";
1096      else
1097        s = cc.getCoding().get(0).getCode();
1098    }
1099    return s;
1100  }
1101
1102  protected void renderCodeableConcept(XhtmlNode x, CodeableConcept cc) throws FHIRFormatError, DefinitionException, IOException {
1103    renderCodeableConcept(x, cc, false);
1104  }
1105  
1106  protected void renderCodeableReference(XhtmlNode x, CodeableReference e, boolean showCodeDetails) throws FHIRFormatError, DefinitionException, IOException {
1107    if (e.hasConcept()) {
1108      renderCodeableConcept(x, e.getConcept(), showCodeDetails);
1109    }
1110    if (e.hasReference()) {
1111      renderReference(x, e.getReference());
1112    }
1113  }
1114
1115  protected void renderCodeableConcept(XhtmlNode x, CodeableConcept cc, boolean showCodeDetails) throws FHIRFormatError, DefinitionException, IOException {
1116    if (cc.isEmpty()) {
1117      return;
1118    }
1119
1120    String s = cc.getText();
1121    if (Utilities.noString(s)) {
1122      for (Coding c : cc.getCoding()) {
1123        if (c.hasDisplayElement()) {
1124          s = c.getDisplay();
1125          break;
1126        }
1127      }
1128    }
1129    if (Utilities.noString(s)) {
1130      // still? ok, let's try looking it up
1131      for (Coding c : cc.getCoding()) {
1132        if (c.hasCodeElement() && c.hasSystemElement()) {
1133          s = lookupCode(c.getSystem(), c.getVersion(), c.getCode());
1134          if (!Utilities.noString(s))
1135            break;
1136        }
1137      }
1138    }
1139
1140    if (Utilities.noString(s)) {
1141      if (cc.getCoding().isEmpty())
1142        s = "";
1143      else
1144        s = cc.getCoding().get(0).getCode();
1145    }
1146
1147    if (showCodeDetails) {
1148      x.addText(s+" ");
1149      XhtmlNode sp = x.span("background: LightGoldenRodYellow; margin: 4px; border: 1px solid khaki", null);
1150      sp.tx(" (");
1151      boolean first = true;
1152      for (Coding c : cc.getCoding()) {
1153        if (first) {
1154          first = false;
1155        } else {
1156          sp.tx("; ");
1157        }
1158        String url = getLinkForSystem(c.getSystem(), c.getVersion());
1159        if (url != null) {
1160          sp.ah(url).tx(displayCodeSource(c.getSystem(), c.getVersion()));
1161        } else {
1162          sp.tx(displayCodeSource(c.getSystem(), c.getVersion()));
1163        }
1164        if (c.hasCode()) {
1165          sp.tx("#"+c.getCode());
1166        }
1167        if (c.hasDisplay() && !s.equals(c.getDisplay())) {
1168          sp.tx(" \""+c.getDisplay()+"\"");
1169        }
1170      }
1171      if (hasRenderableExtensions(cc)) {
1172        if (!first) {
1173          sp.tx("; ");
1174        }
1175        renderExtensionsInText(sp, cc, ";");
1176      }
1177      sp.tx(")");
1178    } else {
1179
1180      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1181      for (Coding c : cc.getCoding()) {
1182        if (c.hasCodeElement() && c.hasSystemElement()) {
1183          b.append("{"+c.getSystem()+" "+c.getCode()+"}");
1184        }
1185      }
1186
1187      x.span(null, "Codes: "+b.toString()).addText(s);
1188    }
1189  }
1190
1191  private String displayIdentifier(Identifier ii) {
1192    String s = Utilities.noString(ii.getValue()) ? "?ngen-9?" : ii.getValue();
1193
1194    if (ii.hasType()) {
1195      if (ii.getType().hasText())
1196        s = ii.getType().getText()+":\u00A0"+s;
1197      else if (ii.getType().hasCoding() && ii.getType().getCoding().get(0).hasDisplay())
1198        s = ii.getType().getCoding().get(0).getDisplay()+": "+s;
1199      else if (ii.getType().hasCoding() && ii.getType().getCoding().get(0).hasCode())
1200        s = lookupCode(ii.getType().getCoding().get(0).getSystem(), ii.getType().getCoding().get(0).getVersion(), ii.getType().getCoding().get(0).getCode())+": "+s;
1201    } else {
1202      s = "id:\u00A0"+s;      
1203    }
1204
1205    if (ii.hasUse() || ii.hasPeriod()) {
1206      s = s + "\u00A0(";
1207      if (ii.hasUse()) {
1208        s = s + "use:\u00A0"+ii.getUse().toString();
1209      }
1210      if (ii.hasUse() && ii.hasPeriod()) {
1211        s = s + ",\u00A0";
1212      }
1213      if (ii.hasPeriod()) {
1214        s = s + "period:\u00A0"+displayPeriod(ii.getPeriod());
1215      }
1216      s = s + ")";
1217    }    
1218    return s;
1219  }
1220  
1221  protected void renderIdentifier(XhtmlNode x, Identifier ii) {
1222    x.addText(displayIdentifier(ii));
1223  }
1224
1225  public static String displayHumanName(HumanName name) {
1226    StringBuilder s = new StringBuilder();
1227    if (name.hasText())
1228      s.append(name.getText());
1229    else {
1230      for (StringType p : name.getGiven()) {
1231        s.append(p.getValue());
1232        s.append(" ");
1233      }
1234      if (name.hasFamily()) {
1235        s.append(name.getFamily());
1236        s.append(" ");
1237      }
1238    }
1239    if (name.hasUse() && name.getUse() != NameUse.USUAL)
1240      s.append("("+name.getUse().toString()+")");
1241    return s.toString();
1242  }
1243
1244
1245  protected void renderHumanName(XhtmlNode x, HumanName name) {
1246    x.addText(displayHumanName(name));
1247  }
1248
1249  private String displayAddress(Address address) {
1250    StringBuilder s = new StringBuilder();
1251    if (address.hasText())
1252      s.append(address.getText());
1253    else {
1254      for (StringType p : address.getLine()) {
1255        s.append(p.getValue());
1256        s.append(" ");
1257      }
1258      if (address.hasCity()) {
1259        s.append(address.getCity());
1260        s.append(" ");
1261      }
1262      if (address.hasState()) {
1263        s.append(address.getState());
1264        s.append(" ");
1265      }
1266
1267      if (address.hasPostalCode()) {
1268        s.append(address.getPostalCode());
1269        s.append(" ");
1270      }
1271
1272      if (address.hasCountry()) {
1273        s.append(address.getCountry());
1274        s.append(" ");
1275      }
1276    }
1277    if (address.hasUse())
1278      s.append("("+address.getUse().toString()+")");
1279    return s.toString();
1280  }
1281  
1282  protected void renderAddress(XhtmlNode x, Address address) {
1283    x.addText(displayAddress(address));
1284  }
1285
1286
1287  public static String displayContactPoint(ContactPoint contact) {
1288    StringBuilder s = new StringBuilder();
1289    s.append(describeSystem(contact.getSystem()));
1290    if (Utilities.noString(contact.getValue()))
1291      s.append("-unknown-");
1292    else
1293      s.append(contact.getValue());
1294    if (contact.hasUse())
1295      s.append("("+contact.getUse().toString()+")");
1296    return s.toString();
1297  }
1298
1299  protected String getLocalizedBigDecimalValue(BigDecimal input, Currency c) {
1300    NumberFormat numberFormat = NumberFormat.getNumberInstance(context.getLocale());
1301    numberFormat.setGroupingUsed(true);
1302    numberFormat.setMaximumFractionDigits(c.getDefaultFractionDigits());
1303    numberFormat.setMinimumFractionDigits(c.getDefaultFractionDigits());
1304    return numberFormat.format(input);
1305}
1306  
1307  protected void renderMoney(XhtmlNode x, Money money) {
1308    if (x.getName().equals("blockquote")) {
1309      x = x.para();
1310    }
1311    Currency c = Currency.getInstance(money.getCurrency());
1312    if (c != null) {
1313      XhtmlNode s = x.span(null, c.getDisplayName());
1314      s.tx(c.getSymbol(context.getLocale()));
1315      s.tx(getLocalizedBigDecimalValue(money.getValue(), c));
1316      x.tx(" ("+c.getCurrencyCode()+")");
1317    } else {
1318      x.tx(money.getCurrency());
1319      x.tx(money.getValue().toPlainString());
1320    }
1321  }
1322  
1323  protected void renderExpression(XhtmlNode x, Expression expr) {
1324  // there's two parts: what the expression is, and how it's described. 
1325    // we start with what it is, and then how it's described 
1326    XhtmlNode p = x;
1327    if (p.getName().equals("blockquote")) {
1328      p = p.para();
1329    }
1330    if (expr.hasExpression()) {
1331      if (expr.hasReference()) {
1332        p = x.ah(expr.getReference());        
1333      }
1334      XhtmlNode c = p;
1335      if (expr.hasLanguage()) {
1336        c = c.span(null, expr.getLanguage());
1337      }
1338      c.code().tx(expr.getExpression());
1339    } else if (expr.hasReference()) {
1340      p.ah(expr.getReference()).tx("source");
1341    }
1342    if (expr.hasName() || expr.hasDescription()) {
1343      p.tx("(");
1344      if (expr.hasName()) {
1345        p.b().tx(expr.getName());
1346      }
1347      if (expr.hasDescription()) {
1348        p.tx("\"");
1349        p.tx(expr.getDescription());
1350        p.tx("\"");
1351      }
1352      p.tx(")");
1353    }
1354  }
1355  
1356  
1357  protected void renderContactPoint(XhtmlNode x, ContactPoint contact) {
1358    if (contact != null) {
1359      if (!contact.hasSystem()) {
1360        x.addText(displayContactPoint(contact));        
1361      } else {
1362        switch (contact.getSystem()) {
1363        case EMAIL:
1364          x.ah("mailto:"+contact.getValue()).tx(contact.getValue());
1365          break;
1366        case FAX:
1367          x.addText(displayContactPoint(contact));
1368          break;
1369        case NULL:
1370          x.addText(displayContactPoint(contact));
1371          break;
1372        case OTHER:
1373          x.addText(displayContactPoint(contact));
1374          break;
1375        case PAGER:
1376          x.addText(displayContactPoint(contact));
1377          break;
1378        case PHONE:
1379          if (contact.hasValue() && contact.getValue().startsWith("+")) {
1380            x.ah("tel:"+contact.getValue().replace(" ", "")).tx(contact.getValue());
1381          } else {
1382            x.addText(displayContactPoint(contact));
1383          }
1384          break;
1385        case SMS:
1386          x.addText(displayContactPoint(contact));
1387          break;
1388        case URL:
1389          x.ah(contact.getValue()).tx(contact.getValue());
1390          break;
1391        default:
1392          break;      
1393        }
1394      }
1395    }
1396  }
1397
1398  protected void displayContactPoint(XhtmlNode p, ContactPoint c) {
1399    if (c != null) {
1400      if (c.getSystem() == ContactPointSystem.PHONE) {
1401        p.tx("Phone: "+c.getValue());
1402      } else if (c.getSystem() == ContactPointSystem.FAX) {
1403        p.tx("Fax: "+c.getValue());
1404      } else if (c.getSystem() == ContactPointSystem.EMAIL) {
1405        p.tx(c.getValue());
1406      } else if (c.getSystem() == ContactPointSystem.URL) {
1407        if (c.getValue().length() > 30) {
1408          p.addText(c.getValue().substring(0, 30)+"...");
1409        } else {
1410          p.addText(c.getValue());
1411        }
1412      }
1413    }
1414  }
1415
1416  protected void addTelecom(XhtmlNode p, ContactPoint c) {
1417    if (c.getSystem() == ContactPointSystem.PHONE) {
1418      p.tx("Phone: "+c.getValue());
1419    } else if (c.getSystem() == ContactPointSystem.FAX) {
1420      p.tx("Fax: "+c.getValue());
1421    } else if (c.getSystem() == ContactPointSystem.EMAIL) {
1422      p.ah( "mailto:"+c.getValue()).addText(c.getValue());
1423    } else if (c.getSystem() == ContactPointSystem.URL) {
1424      if (c.getValue().length() > 30)
1425        p.ah(c.getValue()).addText(c.getValue().substring(0, 30)+"...");
1426      else
1427        p.ah(c.getValue()).addText(c.getValue());
1428    }
1429  }
1430  private static String describeSystem(ContactPointSystem system) {
1431    if (system == null)
1432      return "";
1433    switch (system) {
1434    case PHONE: return "ph: ";
1435    case FAX: return "fax: ";
1436    default:
1437      return "";
1438    }
1439  }
1440
1441  protected String displayQuantity(Quantity q) {
1442    StringBuilder s = new StringBuilder();
1443
1444    s.append(q.hasValue() ? q.getValue() : "?");
1445    if (q.hasUnit())
1446      s.append(" ").append(q.getUnit());
1447    else if (q.hasCode())
1448      s.append(" ").append(q.getCode());
1449
1450    return s.toString();
1451  }  
1452  
1453  protected void renderQuantity(XhtmlNode x, Quantity q) {
1454    renderQuantity(x, q, false);
1455  }
1456  
1457  protected void renderQuantity(XhtmlNode x, Quantity q, boolean showCodeDetails) {
1458    if (q.hasComparator())
1459      x.addText(q.getComparator().toCode());
1460    if (q.hasValue()) {
1461      x.addText(q.getValue().toString());
1462    }
1463    if (q.hasUnit())
1464      x.tx(" "+q.getUnit());
1465    else if (q.hasCode() && q.hasSystem()) {
1466      // if there's a code there *shall* be a system, so if we've got one and not the other, things are invalid and we won't bother trying to render
1467      if (q.hasSystem() && q.getSystem().equals("http://unitsofmeasure.org"))
1468        x.tx(" "+q.getCode());
1469      else
1470        x.tx("(unit "+q.getCode()+" from "+q.getSystem()+")");
1471    }
1472    if (showCodeDetails && q.hasCode()) {
1473      x.span("background: LightGoldenRodYellow", null).tx(" (Details: "+TerminologyRenderer.describeSystem(q.getSystem())+" code "+q.getCode()+" = '"+lookupCode(q.getSystem(), null, q.getCode())+"')");
1474    }
1475  }
1476
1477  public String displayRange(Range q) {
1478    if (!q.hasLow() && !q.hasHigh())
1479      return "?";
1480
1481    StringBuilder b = new StringBuilder();
1482
1483    boolean sameUnits = (q.getLow().hasUnit() && q.getHigh().hasUnit() && q.getLow().getUnit().equals(q.getHigh().getUnit())) 
1484        || (q.getLow().hasCode() && q.getHigh().hasCode() && q.getLow().getCode().equals(q.getHigh().getCode()));
1485    String low = "?";
1486    if (q.hasLow() && q.getLow().hasValue())
1487      low = sameUnits ? q.getLow().getValue().toString() : displayQuantity(q.getLow());
1488    String high = displayQuantity(q.getHigh());
1489    if (high.isEmpty())
1490      high = "?";
1491    b.append(low).append("\u00A0to\u00A0").append(high);
1492    return b.toString();
1493  }
1494
1495  protected void renderRange(XhtmlNode x, Range q) {
1496    if (q.hasLow())
1497      x.addText(q.getLow().getValue().toString());
1498    else
1499      x.tx("?");
1500    x.tx("-");
1501    if (q.hasHigh())
1502      x.addText(q.getHigh().getValue().toString());
1503    else
1504      x.tx("?");
1505    if (q.getLow().hasUnit())
1506      x.tx(" "+q.getLow().getUnit());
1507  }
1508
1509  public String displayPeriod(Period p) {
1510    String s = !p.hasStart() ? "(?)" : displayDateTime(p.getStartElement());
1511    s = s + " --> ";
1512    return s + (!p.hasEnd() ? "(ongoing)" : displayDateTime(p.getEndElement()));
1513  }
1514
1515  public void renderPeriod(XhtmlNode x, Period p) {
1516    x.addText(!p.hasStart() ? "??" : displayDateTime(p.getStartElement()));
1517    x.tx(" --> ");
1518    x.addText(!p.hasEnd() ? "(ongoing)" : displayDateTime(p.getEndElement()));
1519  }
1520  
1521  public void renderUsageContext(XhtmlNode x, UsageContext u) throws FHIRFormatError, DefinitionException, IOException {
1522    renderCoding(x,  u.getCode());
1523    x.tx(": ");
1524    render(x, u.getValue());    
1525  }
1526  
1527  public void renderDataRequirement(XhtmlNode x, DataRequirement dr) throws FHIRFormatError, DefinitionException, IOException {
1528    XhtmlNode tbl = x.table("grid");
1529    XhtmlNode tr = tbl.tr();    
1530    XhtmlNode td = tr.td().colspan("2");
1531    td.b().tx("Type");
1532    td.tx(": ");
1533    StructureDefinition sd = context.getWorker().fetchTypeDefinition(dr.getType().toCode());
1534    if (sd != null && sd.hasWebPath()) {
1535      td.ah(sd.getWebPath()).tx(dr.getType().toCode());
1536    } else {
1537      td.tx(dr.getType().toCode());
1538    }
1539    if (dr.hasProfile()) {
1540      td.tx(" (");
1541      boolean first = true;
1542      for (CanonicalType p : dr.getProfile()) {
1543        if (first) first = false; else td.tx(" | ");
1544        sd = context.getWorker().fetchResource(StructureDefinition.class, p.getValue());
1545        if (sd != null && sd.hasWebPath()) {
1546          td.ah(sd.getWebPath()).tx(sd.present());
1547        } else {
1548            td.tx(p.asStringValue());
1549        }
1550      }
1551      td.tx(")");
1552    }
1553    if (dr.hasSubject()) {
1554      tr = tbl.tr();    
1555      td = tr.td().colspan("2");
1556      td.b().tx("Subject");
1557      if (dr.hasSubjectReference()) {
1558        renderReference(td,  dr.getSubjectReference());
1559      } else {
1560        renderCodeableConcept(td, dr.getSubjectCodeableConcept());
1561      }
1562    }
1563    if (dr.hasCodeFilter() || dr.hasDateFilter()) {
1564      tr = tbl.tr().backgroundColor("#efefef");    
1565      tr.td().tx("Filter");
1566      tr.td().tx("Value");
1567    }
1568    for (DataRequirementCodeFilterComponent cf : dr.getCodeFilter()) {
1569      tr = tbl.tr();    
1570      if (cf.hasPath()) {
1571        tr.td().tx(cf.getPath());
1572      } else {
1573        tr.td().tx("Search on " +cf.getSearchParam());
1574      }
1575      if (cf.hasValueSet()) {
1576        td = tr.td();
1577        td.tx("In ValueSet ");
1578        render(td, cf.getValueSetElement());
1579      } else {
1580        boolean first = true;
1581        td = tr.td();
1582        td.tx("One of these codes: ");
1583        for (Coding c : cf.getCode()) {
1584          if (first) first = false; else td.tx(", ");
1585          render(td, c);
1586        }
1587      }
1588    }
1589    for (DataRequirementDateFilterComponent cf : dr.getDateFilter()) {
1590      tr = tbl.tr();    
1591      if (cf.hasPath()) {
1592        tr.td().tx(cf.getPath());
1593      } else {
1594        tr.td().tx("Search on " +cf.getSearchParam());
1595      }
1596      render(tr.td(), cf.getValue());
1597    }
1598    if (dr.hasSort() || dr.hasLimit()) {
1599      tr = tbl.tr();    
1600      td = tr.td().colspan("2");
1601      if (dr.hasLimit()) {
1602        td.b().tx("Limit");
1603        td.tx(": ");
1604        td.tx(dr.getLimit());
1605        if (dr.hasSort()) {
1606          td.tx(", ");
1607        }
1608      }
1609      if (dr.hasSort()) {
1610        td.b().tx("Sort");
1611        td.tx(": ");
1612        boolean first = true;
1613        for (DataRequirementSortComponent p : dr.getSort()) {
1614          if (first) first = false; else td.tx(" | ");
1615          td.tx(p.getDirection() == SortDirection.ASCENDING ? "+" : "-");
1616          td.tx(p.getPath());
1617        }
1618      }
1619    }
1620  }
1621  
1622  
1623  private String displayTiming(Timing s) throws FHIRException {
1624    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1625    if (s.hasCode())
1626      b.append("Code: "+displayCodeableConcept(s.getCode()));
1627
1628    if (s.getEvent().size() > 0) {
1629      CommaSeparatedStringBuilder c = new CommaSeparatedStringBuilder();
1630      for (DateTimeType p : s.getEvent()) {
1631        if (p.hasValue()) {
1632          c.append(displayDateTime(p));
1633        } else if (!renderExpression(c, p)) {
1634          c.append("??");
1635        }        
1636      }
1637      b.append("Events: "+ c.toString());
1638    }
1639
1640    if (s.hasRepeat()) {
1641      TimingRepeatComponent rep = s.getRepeat();
1642      if (rep.hasBoundsPeriod() && rep.getBoundsPeriod().hasStart())
1643        b.append("Starting "+displayDateTime(rep.getBoundsPeriod().getStartElement()));
1644      if (rep.hasCount())
1645        b.append("Count "+Integer.toString(rep.getCount())+" times");
1646      if (rep.hasDuration())
1647        b.append("Duration "+rep.getDuration().toPlainString()+displayTimeUnits(rep.getPeriodUnit()));
1648
1649      if (rep.hasWhen()) {
1650        String st = "";
1651        if (rep.hasOffset()) {
1652          st = Integer.toString(rep.getOffset())+"min ";
1653        }
1654        b.append(st);
1655        for (Enumeration<EventTiming> wh : rep.getWhen())
1656          b.append(displayEventCode(wh.getValue()));
1657      } else {
1658        String st = "";
1659        if (!rep.hasFrequency() || (!rep.hasFrequencyMax() && rep.getFrequency() == 1) )
1660          st = "Once";
1661        else {
1662          st = Integer.toString(rep.getFrequency());
1663          if (rep.hasFrequencyMax())
1664            st = st + "-"+Integer.toString(rep.getFrequency());
1665        }
1666        if (rep.hasPeriod()) {
1667          st = st + " per "+rep.getPeriod().toPlainString();
1668          if (rep.hasPeriodMax())
1669            st = st + "-"+rep.getPeriodMax().toPlainString();
1670          st = st + " "+displayTimeUnits(rep.getPeriodUnit());
1671        }
1672        b.append(st);
1673      }
1674      if (rep.hasBoundsPeriod() && rep.getBoundsPeriod().hasEnd())
1675        b.append("Until "+displayDateTime(rep.getBoundsPeriod().getEndElement()));
1676    }
1677    return b.toString();
1678  }
1679
1680  private boolean renderExpression(CommaSeparatedStringBuilder c, PrimitiveType p) {
1681    Extension exp = p.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/cqf-expression");
1682    if (exp == null) {
1683      return false;
1684    }
1685    c.append(exp.getValueExpression().getExpression());
1686    return true;
1687  }
1688
1689  private String displayEventCode(EventTiming when) {
1690    switch (when) {
1691    case C: return "at meals";
1692    case CD: return "at lunch";
1693    case CM: return "at breakfast";
1694    case CV: return "at dinner";
1695    case AC: return "before meals";
1696    case ACD: return "before lunch";
1697    case ACM: return "before breakfast";
1698    case ACV: return "before dinner";
1699    case HS: return "before sleeping";
1700    case PC: return "after meals";
1701    case PCD: return "after lunch";
1702    case PCM: return "after breakfast";
1703    case PCV: return "after dinner";
1704    case WAKE: return "after waking";
1705    default: return "?ngen-6?";
1706    }
1707  }
1708
1709  private String displayTimeUnits(UnitsOfTime units) {
1710    if (units == null)
1711      return "?ngen-7?";
1712    switch (units) {
1713    case A: return "years";
1714    case D: return "days";
1715    case H: return "hours";
1716    case MIN: return "minutes";
1717    case MO: return "months";
1718    case S: return "seconds";
1719    case WK: return "weeks";
1720    default: return "?ngen-8?";
1721    }
1722  }
1723  
1724  protected void renderTiming(XhtmlNode x, Timing s) throws FHIRException {
1725    x.addText(displayTiming(s));
1726  }
1727
1728
1729  private String displaySampledData(SampledData s) {
1730    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1731    if (s.hasOrigin())
1732      b.append("Origin: "+displayQuantity(s.getOrigin()));
1733
1734    if (s.hasInterval()) {
1735      b.append("Interval: "+s.getInterval().toString());
1736
1737      if (s.hasIntervalUnit())
1738        b.append(s.getIntervalUnit().toString());
1739    }
1740
1741    if (s.hasFactor())
1742      b.append("Factor: "+s.getFactor().toString());
1743
1744    if (s.hasLowerLimit())
1745      b.append("Lower: "+s.getLowerLimit().toString());
1746
1747    if (s.hasUpperLimit())
1748      b.append("Upper: "+s.getUpperLimit().toString());
1749
1750    if (s.hasDimensions())
1751      b.append("Dimensions: "+s.getDimensions());
1752
1753    if (s.hasData())
1754      b.append("Data: "+s.getData());
1755
1756    return b.toString();
1757  }
1758
1759  protected void renderSampledData(XhtmlNode x, SampledData sampledData) {
1760    x.addText(displaySampledData(sampledData));
1761  }
1762
1763  public RenderingContext getContext() {
1764    return context;
1765  }
1766  
1767
1768  public XhtmlNode makeExceptionXhtml(Exception e, String function) {
1769    XhtmlNode xn;
1770    xn = new XhtmlNode(NodeType.Element, "div");
1771    XhtmlNode p = xn.para();
1772    p.b().tx("Exception "+function+": "+e.getMessage());
1773    p.addComment(getStackTrace(e));
1774    return xn;
1775  }
1776
1777  private String getStackTrace(Exception e) {
1778    StringBuilder b = new StringBuilder();
1779    b.append("\r\n");
1780    for (StackTraceElement t : e.getStackTrace()) {
1781      b.append(t.getClassName()+"."+t.getMethodName()+" ("+t.getFileName()+":"+t.getLineNumber());
1782      b.append("\r\n");
1783    }
1784    return b.toString();
1785  }
1786
1787  protected String versionFromCanonical(String system) {
1788    if (system == null) {
1789      return null;
1790    } else if (system.contains("|")) {
1791      return system.substring(0, system.indexOf("|"));
1792    } else {
1793      return null;
1794    }
1795  }
1796
1797  protected String systemFromCanonical(String system) {
1798    if (system == null) {
1799      return null;
1800    } else if (system.contains("|")) {
1801      return system.substring(system.indexOf("|")+1);
1802    } else {
1803      return system;
1804    }
1805  }
1806
1807
1808}