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