001package org.hl7.fhir.r5.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.r5.context.IWorkerContext;
022import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
023import org.hl7.fhir.r5.model.Address;
024import org.hl7.fhir.r5.model.Annotation;
025import org.hl7.fhir.r5.model.Base;
026import org.hl7.fhir.r5.model.BaseDateTimeType;
027import org.hl7.fhir.r5.model.CanonicalResource;
028import org.hl7.fhir.r5.model.CanonicalType;
029import org.hl7.fhir.r5.model.CodeSystem;
030import org.hl7.fhir.r5.model.CodeableConcept;
031import org.hl7.fhir.r5.model.CodeableReference;
032import org.hl7.fhir.r5.model.Coding;
033import org.hl7.fhir.r5.model.ContactPoint;
034import org.hl7.fhir.r5.model.DataRequirement;
035import org.hl7.fhir.r5.model.DataRequirement.DataRequirementCodeFilterComponent;
036import org.hl7.fhir.r5.model.DataRequirement.DataRequirementDateFilterComponent;
037import org.hl7.fhir.r5.model.DataRequirement.DataRequirementSortComponent;
038import org.hl7.fhir.r5.model.DataRequirement.SortDirection;
039import org.hl7.fhir.r5.model.ContactPoint.ContactPointSystem;
040import org.hl7.fhir.r5.model.DataType;
041import org.hl7.fhir.r5.model.DateTimeType;
042import org.hl7.fhir.r5.model.Enumeration;
043import org.hl7.fhir.r5.model.Expression;
044import org.hl7.fhir.r5.model.Extension;
045import org.hl7.fhir.r5.model.HumanName;
046import org.hl7.fhir.r5.model.HumanName.NameUse;
047import org.hl7.fhir.r5.model.IdType;
048import org.hl7.fhir.r5.model.Identifier;
049import org.hl7.fhir.r5.model.MarkdownType;
050import org.hl7.fhir.r5.model.Money;
051import org.hl7.fhir.r5.model.Period;
052import org.hl7.fhir.r5.model.PrimitiveType;
053import org.hl7.fhir.r5.model.Quantity;
054import org.hl7.fhir.r5.model.Range;
055import org.hl7.fhir.r5.model.Reference;
056import org.hl7.fhir.r5.model.Resource;
057import org.hl7.fhir.r5.model.SampledData;
058import org.hl7.fhir.r5.model.StringType;
059import org.hl7.fhir.r5.model.StructureDefinition;
060import org.hl7.fhir.r5.model.Timing;
061import org.hl7.fhir.r5.model.Timing.EventTiming;
062import org.hl7.fhir.r5.model.Timing.TimingRepeatComponent;
063import org.hl7.fhir.r5.model.Timing.UnitsOfTime;
064import org.hl7.fhir.r5.model.UriType;
065import org.hl7.fhir.r5.model.ValueSet;
066import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent;
067import org.hl7.fhir.r5.model.ValueSet.ConceptReferenceDesignationComponent;
068import org.hl7.fhir.r5.renderers.utils.BaseWrappers.BaseWrapper;
069import org.hl7.fhir.r5.renderers.utils.RenderingContext;
070import org.hl7.fhir.r5.renderers.utils.RenderingContext.ResourceRendererMode;
071import org.hl7.fhir.r5.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(q.hasValue() ? q.getValue() : "?");
1143    if (q.hasUnit())
1144      s.append(" ").append(q.getUnit());
1145    else if (q.hasCode())
1146      s.append(" ").append(q.getCode());
1147
1148    return s.toString();
1149  }  
1150  
1151  protected void renderQuantity(XhtmlNode x, Quantity q) {
1152    renderQuantity(x, q, false);
1153  }
1154  
1155  protected void renderQuantity(XhtmlNode x, Quantity q, boolean showCodeDetails) {
1156    if (q.hasComparator())
1157      x.addText(q.getComparator().toCode());
1158    if (q.hasValue()) {
1159      x.addText(q.getValue().toString());
1160    }
1161    if (q.hasUnit())
1162      x.tx(" "+q.getUnit());
1163    else if (q.hasCode() && q.hasSystem()) {
1164      // 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
1165      if (q.hasSystem() && q.getSystem().equals("http://unitsofmeasure.org"))
1166        x.tx(" "+q.getCode());
1167      else
1168        x.tx("(unit "+q.getCode()+" from "+q.getSystem()+")");
1169    }
1170    if (showCodeDetails && q.hasCode()) {
1171      x.span("background: LightGoldenRodYellow", null).tx(" (Details: "+TerminologyRenderer.describeSystem(q.getSystem())+" code "+q.getCode()+" = '"+lookupCode(q.getSystem(), null, q.getCode())+"')");
1172    }
1173  }
1174
1175  public String displayRange(Range q) {
1176    if (!q.hasLow() && !q.hasHigh())
1177      return "?";
1178
1179    StringBuilder b = new StringBuilder();
1180
1181    boolean sameUnits = (q.getLow().hasUnit() && q.getHigh().hasUnit() && q.getLow().getUnit().equals(q.getHigh().getUnit())) 
1182        || (q.getLow().hasCode() && q.getHigh().hasCode() && q.getLow().getCode().equals(q.getHigh().getCode()));
1183    String low = "?";
1184    if (q.hasLow() && q.getLow().hasValue())
1185      low = sameUnits ? q.getLow().getValue().toString() : displayQuantity(q.getLow());
1186    String high = displayQuantity(q.getHigh());
1187    if (high.isEmpty())
1188      high = "?";
1189    b.append(low).append("\u00A0to\u00A0").append(high);
1190    return b.toString();
1191  }
1192
1193  protected void renderRange(XhtmlNode x, Range q) {
1194    if (q.hasLow())
1195      x.addText(q.getLow().getValue().toString());
1196    else
1197      x.tx("?");
1198    x.tx("-");
1199    if (q.hasHigh())
1200      x.addText(q.getHigh().getValue().toString());
1201    else
1202      x.tx("?");
1203    if (q.getLow().hasUnit())
1204      x.tx(" "+q.getLow().getUnit());
1205  }
1206
1207  public String displayPeriod(Period p) {
1208    String s = !p.hasStart() ? "(?)" : displayDateTime(p.getStartElement());
1209    s = s + " --> ";
1210    return s + (!p.hasEnd() ? "(ongoing)" : displayDateTime(p.getEndElement()));
1211  }
1212
1213  public void renderPeriod(XhtmlNode x, Period p) {
1214    x.addText(!p.hasStart() ? "??" : displayDateTime(p.getStartElement()));
1215    x.tx(" --> ");
1216    x.addText(!p.hasEnd() ? "(ongoing)" : displayDateTime(p.getEndElement()));
1217  }
1218  
1219  public void renderDataRequirement(XhtmlNode x, DataRequirement dr) throws FHIRFormatError, DefinitionException, IOException {
1220    XhtmlNode tbl = x.table("grid");
1221    XhtmlNode tr = tbl.tr();    
1222    XhtmlNode td = tr.td().colspan("2");
1223    td.b().tx("Type");
1224    td.tx(": ");
1225    StructureDefinition sd = context.getWorker().fetchTypeDefinition(dr.getType().toCode());
1226    if (sd != null && sd.hasUserData("path")) {
1227      td.ah(sd.getUserString("path")).tx(dr.getType().toCode());
1228    } else {
1229      td.tx(dr.getType().toCode());
1230    }
1231    if (dr.hasProfile()) {
1232      td.tx(" (");
1233      boolean first = true;
1234      for (CanonicalType p : dr.getProfile()) {
1235        if (first) first = false; else td.tx(" | ");
1236        sd = context.getWorker().fetchResource(StructureDefinition.class, p.getValue());
1237        if (sd != null && sd.hasUserData("path")) {
1238          td.ah(sd.getUserString("path")).tx(sd.present());
1239        } else {
1240            td.tx(p.asStringValue());
1241        }
1242      }
1243      td.tx(")");
1244    }
1245    if (dr.hasSubject()) {
1246      tr = tbl.tr();    
1247      td = tr.td().colspan("2");
1248      td.b().tx("Subject");
1249      if (dr.hasSubjectReference()) {
1250        renderReference(td,  dr.getSubjectReference());
1251      } else {
1252        renderCodeableConcept(td, dr.getSubjectCodeableConcept());
1253      }
1254    }
1255    if (dr.hasCodeFilter() || dr.hasDateFilter()) {
1256      tr = tbl.tr().backgroundColor("#efefef");    
1257      tr.td().tx("Filter");
1258      tr.td().tx("Value");
1259    }
1260    for (DataRequirementCodeFilterComponent cf : dr.getCodeFilter()) {
1261      tr = tbl.tr();    
1262      if (cf.hasPath()) {
1263        tr.td().tx(cf.getPath());
1264      } else {
1265        tr.td().tx("Search on " +cf.getSearchParam());
1266      }
1267      if (cf.hasValueSet()) {
1268        td = tr.td();
1269        td.tx("In ValueSet ");
1270        render(td, cf.getValueSetElement());
1271      } else {
1272        boolean first = true;
1273        td = tr.td();
1274        td.tx("One of these codes: ");
1275        for (Coding c : cf.getCode()) {
1276          if (first) first = false; else td.tx(", ");
1277          render(td, c);
1278        }
1279      }
1280    }
1281    for (DataRequirementDateFilterComponent cf : dr.getDateFilter()) {
1282      tr = tbl.tr();    
1283      if (cf.hasPath()) {
1284        tr.td().tx(cf.getPath());
1285      } else {
1286        tr.td().tx("Search on " +cf.getSearchParam());
1287      }
1288      render(tr.td(), cf.getValue());
1289    }
1290    if (dr.hasSort() || dr.hasLimit()) {
1291      tr = tbl.tr();    
1292      td = tr.td().colspan("2");
1293      if (dr.hasLimit()) {
1294        td.b().tx("Limit");
1295        td.tx(": ");
1296        td.tx(dr.getLimit());
1297        if (dr.hasSort()) {
1298          td.tx(", ");
1299        }
1300      }
1301      if (dr.hasSort()) {
1302        td.b().tx("Sort");
1303        td.tx(": ");
1304        boolean first = true;
1305        for (DataRequirementSortComponent p : dr.getSort()) {
1306          if (first) first = false; else td.tx(" | ");
1307          td.tx(p.getDirection() == SortDirection.ASCENDING ? "+" : "-");
1308          td.tx(p.getPath());
1309        }
1310      }
1311    }
1312  }
1313  
1314  
1315  private String displayTiming(Timing s) throws FHIRException {
1316    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1317    if (s.hasCode())
1318      b.append("Code: "+displayCodeableConcept(s.getCode()));
1319
1320    if (s.getEvent().size() > 0) {
1321      CommaSeparatedStringBuilder c = new CommaSeparatedStringBuilder();
1322      for (DateTimeType p : s.getEvent()) {
1323        if (p.hasValue()) {
1324          c.append(displayDateTime(p));
1325        } else if (!renderExpression(c, p)) {
1326          c.append("??");
1327        }        
1328      }
1329      b.append("Events: "+ c.toString());
1330    }
1331
1332    if (s.hasRepeat()) {
1333      TimingRepeatComponent rep = s.getRepeat();
1334      if (rep.hasBoundsPeriod() && rep.getBoundsPeriod().hasStart())
1335        b.append("Starting "+displayDateTime(rep.getBoundsPeriod().getStartElement()));
1336      if (rep.hasCount())
1337        b.append("Count "+Integer.toString(rep.getCount())+" times");
1338      if (rep.hasDuration())
1339        b.append("Duration "+rep.getDuration().toPlainString()+displayTimeUnits(rep.getPeriodUnit()));
1340
1341      if (rep.hasWhen()) {
1342        String st = "";
1343        if (rep.hasOffset()) {
1344          st = Integer.toString(rep.getOffset())+"min ";
1345        }
1346        b.append("Do "+st);
1347        for (Enumeration<EventTiming> wh : rep.getWhen())
1348          b.append(displayEventCode(wh.getValue()));
1349      } else {
1350        String st = "";
1351        if (!rep.hasFrequency() || (!rep.hasFrequencyMax() && rep.getFrequency() == 1) )
1352          st = "Once";
1353        else {
1354          st = Integer.toString(rep.getFrequency());
1355          if (rep.hasFrequencyMax())
1356            st = st + "-"+Integer.toString(rep.getFrequency());
1357        }
1358        if (rep.hasPeriod()) {
1359          st = st + " per "+rep.getPeriod().toPlainString();
1360          if (rep.hasPeriodMax())
1361            st = st + "-"+rep.getPeriodMax().toPlainString();
1362          st = st + " "+displayTimeUnits(rep.getPeriodUnit());
1363        }
1364        b.append("Do "+st);
1365      }
1366      if (rep.hasBoundsPeriod() && rep.getBoundsPeriod().hasEnd())
1367        b.append("Until "+displayDateTime(rep.getBoundsPeriod().getEndElement()));
1368    }
1369    return b.toString();
1370  }
1371
1372  private boolean renderExpression(CommaSeparatedStringBuilder c, PrimitiveType p) {
1373    Extension exp = p.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/cqf-expression");
1374    if (exp == null) {
1375      return false;
1376    }
1377    c.append(exp.getValueExpression().getExpression());
1378    return true;
1379  }
1380
1381  private String displayEventCode(EventTiming when) {
1382    switch (when) {
1383    case C: return "at meals";
1384    case CD: return "at lunch";
1385    case CM: return "at breakfast";
1386    case CV: return "at dinner";
1387    case AC: return "before meals";
1388    case ACD: return "before lunch";
1389    case ACM: return "before breakfast";
1390    case ACV: return "before dinner";
1391    case HS: return "before sleeping";
1392    case PC: return "after meals";
1393    case PCD: return "after lunch";
1394    case PCM: return "after breakfast";
1395    case PCV: return "after dinner";
1396    case WAKE: return "after waking";
1397    default: return "?ngen-6?";
1398    }
1399  }
1400
1401  private String displayTimeUnits(UnitsOfTime units) {
1402    if (units == null)
1403      return "?ngen-7?";
1404    switch (units) {
1405    case A: return "years";
1406    case D: return "days";
1407    case H: return "hours";
1408    case MIN: return "minutes";
1409    case MO: return "months";
1410    case S: return "seconds";
1411    case WK: return "weeks";
1412    default: return "?ngen-8?";
1413    }
1414  }
1415  
1416  protected void renderTiming(XhtmlNode x, Timing s) throws FHIRException {
1417    x.addText(displayTiming(s));
1418  }
1419
1420
1421  private String displaySampledData(SampledData s) {
1422    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
1423    if (s.hasOrigin())
1424      b.append("Origin: "+displayQuantity(s.getOrigin()));
1425
1426    if (s.hasPeriod())
1427      b.append("Period: "+s.getPeriod().toString());
1428
1429    if (s.hasFactor())
1430      b.append("Factor: "+s.getFactor().toString());
1431
1432    if (s.hasLowerLimit())
1433      b.append("Lower: "+s.getLowerLimit().toString());
1434
1435    if (s.hasUpperLimit())
1436      b.append("Upper: "+s.getUpperLimit().toString());
1437
1438    if (s.hasDimensions())
1439      b.append("Dimensions: "+s.getDimensions());
1440
1441    if (s.hasData())
1442      b.append("Data: "+s.getData());
1443
1444    return b.toString();
1445  }
1446
1447  protected void renderSampledData(XhtmlNode x, SampledData sampledData) {
1448    x.addText(displaySampledData(sampledData));
1449  }
1450
1451  public RenderingContext getContext() {
1452    return context;
1453  }
1454  
1455
1456  public XhtmlNode makeExceptionXhtml(Exception e, String function) {
1457    XhtmlNode xn;
1458    xn = new XhtmlNode(NodeType.Element, "div");
1459    XhtmlNode p = xn.para();
1460    p.b().tx("Exception "+function+": "+e.getMessage());
1461    p.addComment(getStackTrace(e));
1462    return xn;
1463  }
1464
1465  private String getStackTrace(Exception e) {
1466    StringBuilder b = new StringBuilder();
1467    b.append("\r\n");
1468    for (StackTraceElement t : e.getStackTrace()) {
1469      b.append(t.getClassName()+"."+t.getMethodName()+" ("+t.getFileName()+":"+t.getLineNumber());
1470      b.append("\r\n");
1471    }
1472    return b.toString();
1473  }
1474
1475  protected String versionFromCanonical(String system) {
1476    if (system == null) {
1477      return null;
1478    } else if (system.contains("|")) {
1479      return system.substring(0, system.indexOf("|"));
1480    } else {
1481      return null;
1482    }
1483  }
1484
1485  protected String systemFromCanonical(String system) {
1486    if (system == null) {
1487      return null;
1488    } else if (system.contains("|")) {
1489      return system.substring(system.indexOf("|")+1);
1490    } else {
1491      return system;
1492    }
1493  }
1494
1495
1496}