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