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