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