001package org.hl7.fhir.r5.renderers;
002
003import java.io.File;
004import java.io.IOException;
005import java.io.UnsupportedEncodingException;
006import java.util.ArrayList;
007import java.util.HashMap;
008import java.util.List;
009import java.util.Map;
010import java.util.UUID;
011
012import org.hl7.fhir.exceptions.DefinitionException;
013import org.hl7.fhir.exceptions.FHIRException;
014import org.hl7.fhir.exceptions.FHIRFormatError;
015import org.hl7.fhir.r5.model.Address;
016import org.hl7.fhir.r5.model.Attachment;
017import org.hl7.fhir.r5.model.CodeableConcept;
018import org.hl7.fhir.r5.model.ContactPoint;
019import org.hl7.fhir.r5.model.DataType;
020import org.hl7.fhir.r5.model.DateType;
021import org.hl7.fhir.r5.model.Enumeration;
022import org.hl7.fhir.r5.model.Extension;
023import org.hl7.fhir.r5.model.HumanName;
024import org.hl7.fhir.r5.model.HumanName.NameUse;
025import org.hl7.fhir.r5.model.Identifier;
026import org.hl7.fhir.r5.model.Identifier.IdentifierUse;
027import org.hl7.fhir.r5.model.Patient;
028import org.hl7.fhir.r5.model.Period;
029import org.hl7.fhir.r5.model.PrimitiveType;
030import org.hl7.fhir.r5.model.Reference;
031import org.hl7.fhir.r5.model.Resource;
032import org.hl7.fhir.r5.model.StructureDefinition;
033import org.hl7.fhir.r5.renderers.utils.BaseWrappers.BaseWrapper;
034import org.hl7.fhir.r5.renderers.utils.BaseWrappers.PropertyWrapper;
035import org.hl7.fhir.r5.renderers.utils.BaseWrappers.ResourceWrapper;
036import org.hl7.fhir.r5.renderers.utils.RenderingContext;
037import org.hl7.fhir.r5.utils.EOperationOutcome;
038import org.hl7.fhir.utilities.TextFile;
039import org.hl7.fhir.utilities.Utilities;
040import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
041import org.hl7.fhir.utilities.xhtml.XhtmlNode;
042
043public class PatientRenderer extends ResourceRenderer {
044
045  public class NamedReferance {
046
047    private String name;
048    private Reference reference;
049    private BaseWrapper wrapper;
050
051    public NamedReferance(String name, Reference ref, BaseWrapper refw) {
052      this.name = name;
053      this.reference = ref;
054      this.wrapper = refw;
055    }
056
057    public String getName() {
058      return name;
059    }
060
061    public Reference getReference() {
062      return reference;
063    }
064
065    public BaseWrapper getWrapper() {
066      return wrapper;
067    }
068
069  }
070
071  private static final int MAX_IMAGE_LENGTH = 2*1024*1024;
072
073  public PatientRenderer(RenderingContext context) {
074    super(context);
075  }
076
077  public boolean render(XhtmlNode x, Resource dr) throws UnsupportedEncodingException, IOException {
078    describe(x, dr);
079    return false;
080  }
081
082  // name gender DoB (MRN)
083  public String display(Resource dr) {
084    Patient pat = (Patient) dr;
085    Identifier id = null;
086    for (Identifier t : pat.getIdentifier()) {
087      id = chooseId(id, t);
088    }
089    HumanName n = null;
090    for (HumanName t : pat.getName()) {
091      n = chooseName(n, t);
092    }
093    return display(n, pat.hasGender() ? context.getTranslatedCode(pat.getGenderElement(), "http://hl7.org/fhir/administrative-gender") : null, pat.getBirthDateElement(), id);
094  }
095
096  private Identifier chooseId(Identifier oldId, Identifier newId) {
097    if (oldId == null) {
098      return newId;
099    }
100    if (newId == null) {
101      return oldId;
102    }
103    return isPreferred(newId.getUse(), oldId.getUse()) ? newId : oldId;
104  }
105
106  private boolean isPreferred(IdentifierUse newUse, IdentifierUse oldUse) {
107    if (newUse == null && oldUse == null || newUse == oldUse) {
108      return false;
109    }
110    if (newUse == null) {
111      return true;
112    }
113    switch (newUse) {
114    case NULL: return !existsInList(oldUse, IdentifierUse.OFFICIAL, IdentifierUse.USUAL);
115    case OFFICIAL: return !existsInList(oldUse, IdentifierUse.USUAL);
116    case OLD: return !existsInList(oldUse, IdentifierUse.OFFICIAL, IdentifierUse.SECONDARY, IdentifierUse.USUAL);
117    case SECONDARY: return !existsInList(oldUse, IdentifierUse.OFFICIAL, IdentifierUse.USUAL);
118    case TEMP: return !existsInList(oldUse, IdentifierUse.OFFICIAL, IdentifierUse.SECONDARY, IdentifierUse.USUAL);
119    case USUAL: return true;
120    default: return false;
121    }
122  }
123
124  private boolean existsInList(IdentifierUse oldUse, IdentifierUse... values) {
125    for (IdentifierUse value : values) {
126      if (value == oldUse) {
127        return true;
128      }
129    }
130    return false;
131  }
132
133  private HumanName chooseName(HumanName oldName, HumanName newName) {
134    if (oldName == null) {
135      return newName;
136    }
137    if (newName == null) {
138      return oldName;
139    }
140    return isPreferred(newName.getUse(), oldName.getUse()) ? newName : oldName;
141  }
142
143
144  private boolean isPreferred(NameUse newUse, NameUse oldUse) {
145    if (newUse == null && oldUse == null || newUse == oldUse) {
146      return false;
147    }
148    if (newUse == null) {
149      return true;
150    }
151    if (oldUse == null) {
152      return existsInList(newUse, NameUse.OFFICIAL, NameUse.USUAL);
153    }
154    switch (oldUse) {
155      case ANONYMOUS: return existsInList(newUse, NameUse.OFFICIAL, NameUse.USUAL);
156      case MAIDEN: return existsInList(newUse, NameUse.OFFICIAL, NameUse.USUAL);
157      case NICKNAME: return existsInList(newUse, NameUse.OFFICIAL, NameUse.USUAL);
158      case NULL: return existsInList(newUse, NameUse.OFFICIAL, NameUse.USUAL);
159      case OFFICIAL: return existsInList(newUse, NameUse.USUAL);
160      case OLD: return existsInList(newUse, NameUse.OFFICIAL, NameUse.USUAL);
161      case TEMP: return existsInList(newUse, NameUse.OFFICIAL, NameUse.USUAL);
162      case USUAL: return false; 
163    }
164    return false;
165  }
166
167  private boolean existsInList(NameUse oldUse, NameUse... values) {
168    for (NameUse value : values) {
169      if (value == oldUse) {
170        return true;
171      }
172    }
173    return false;
174  }
175
176  @Override
177  public String display(ResourceWrapper pat) throws UnsupportedEncodingException, IOException {
178    Identifier id = null;
179    PropertyWrapper pw = getProperty(pat, "identifier");
180    for (BaseWrapper t : pw.getValues()) {
181      id = chooseId(id, (Identifier) t.getBase());
182    }
183    pw = getProperty(pat, "name");
184    HumanName n = null;
185    for (BaseWrapper t : pw.getValues()) {
186      n = chooseName(n, (HumanName) t);
187    }
188    String gender = null;
189    pw = getProperty(pat, "gender");
190    if (valued(pw)) {
191      gender = context.getTranslatedCode(pw.value().getBase(), "http://hl7.org/fhir/administrative-gender");
192    }
193    DateType dt = null; 
194    pw = getProperty(pat, "birthDate");
195    if (valued(pw)) {
196      dt = (DateType) pw.value().getBase();
197    }
198    return display(n, gender, dt, id);  
199  }
200
201  public void describe(XhtmlNode x, ResourceWrapper pat) throws UnsupportedEncodingException, IOException {
202    Identifier id = null;
203    PropertyWrapper pw = getProperty(pat, "identifier");
204    for (BaseWrapper t : pw.getValues()) {
205      id = chooseId(id, (Identifier) t.getBase());
206    }
207    pw = getProperty(pat, "name");
208    HumanName n = null;
209    for (BaseWrapper t : pw.getValues()) {
210      n = chooseName(n, (HumanName) t.getBase());
211    }
212    String gender = null;
213    pw = getProperty(pat, "gender");
214    if (valued(pw)) {
215      gender = context.getTranslatedCode(pw.value().getBase(), "http://hl7.org/fhir/administrative-gender");
216    }
217    DateType dt = null; 
218    pw = getProperty(pat, "birthDate");
219    if (valued(pw)) {
220      dt = (DateType) pw.value().getBase();
221    }
222    describe(x, n, gender, dt, id);
223  }
224
225
226  private String display(HumanName name, String gender, DateType dob, Identifier id) {
227    StringBuilder b = new StringBuilder();
228    if (name == null) {
229      b.append(display(name));
230    } else {
231      b.append(context.formatPhrase(RenderingContext.PAT_NO_NAME));      
232    }
233    b.append(" ");
234    if (dob == null) {
235      b.append(context.formatPhrase(RenderingContext.PAT_NO_GENDER));
236    } else {
237      b.append(gender);
238    }
239    b.append(", ");
240    if (dob == null) {
241      b.append(context.formatPhrase(RenderingContext.PAT_NO_DOB));
242    } else {
243      b.append(context.formatPhrase(RenderingContext.PAT_DOB, display(dob)));      
244    }
245    if (id != null) {
246      b.append(" ( ");      
247      b.append(display(id));
248      b.append(")");      
249    }
250    return b.toString();
251  }
252  
253  public void describe(XhtmlNode x, HumanName name, String gender, DateType dob, Identifier id) throws UnsupportedEncodingException, IOException {
254    if (name == null) {
255      x.b().tx(context.formatPhrase(RenderingContext.PAT_NO_NAME)); // todo: is this appropriate?  
256    } else {
257      render(x.b(), name);
258    }
259    x.tx(" ");
260    if (gender == null) {
261      x.tx(context.formatPhrase(RenderingContext.PAT_NO_GENDER));
262    } else {
263      x.tx(gender);
264    }
265    x.tx(", ");
266    if (dob == null) {
267      x.tx(context.formatPhrase(RenderingContext.PAT_NO_DOB));
268    } else {
269      x.tx(context.formatPhrase(RenderingContext.PAT_DOB, display(dob)));
270    }
271    if (id != null) {
272      x.tx(" ( ");      
273      render(x, id);
274      x.tx(")");      
275    }
276  }
277
278  @Override
279  public boolean render(XhtmlNode x, ResourceWrapper r) throws IOException, FHIRException, EOperationOutcome {
280    // banner
281    describe(makeBanner(x.para()), r);
282    x.hr();
283    XhtmlNode tbl;
284    if (hasRenderablePhoto(r)) {
285      tbl = x.table("none");
286      XhtmlNode tr = tbl.tr();
287      tbl = tr.td().table("grid");
288      renderPhoto(tr.td(), r);
289    } else {
290      tbl = x.table("grid");
291    }
292    
293    // the table has 4 columns
294    addStatus(tbl, r);
295    addIdentifiers(tbl, r);
296    addNames(tbl, r);
297    addComms(tbl, r);
298    addLangs(tbl, r);
299    addNOKs(tbl, r);
300    addLinks(tbl, r);
301    addExtensions(tbl, r);
302    if (tbl.isEmpty()) {
303      x.remove(tbl);
304    }
305    if (r.has("contained") && context.isTechnicalMode()) {
306      x.hr();
307      x.para().b().tx(context.formatMessagePlural(r.getContained().size(), RenderingContext.PAT_CONTAINED));
308      addContained(x, r.getContained());
309    }
310    return false;
311  }
312
313  private void addContained(XhtmlNode x, List<ResourceWrapper> list) throws FHIRFormatError, DefinitionException, FHIRException, IOException, EOperationOutcome {
314    for (ResourceWrapper c : list) {
315      x.hr();
316      x.an(c.getId());
317      new RendererFactory().factory(c, context).render(x, c);
318    }
319  }
320
321  private void addExtensions(XhtmlNode tbl, ResourceWrapper r) throws UnsupportedEncodingException, FHIRException, IOException {
322    Map<String, List<Extension>> extensions = new HashMap<>();
323    PropertyWrapper pw = getProperty(r, "extension");
324    for (BaseWrapper t : pw.getValues()) {      
325      Extension ext = (Extension) t.getBase();
326      if (!extensions.containsKey(ext.getUrl())) {
327        extensions.put(ext.getUrl(), new ArrayList<>());
328      }
329      extensions.get(ext.getUrl()).add(ext);
330    }
331    for (String url : extensions.keySet()) {
332      StructureDefinition sd = context.getWorker().fetchResource(StructureDefinition.class, url);
333      if (sd != null) {
334        List<Extension> list = extensions.get(url);
335        boolean anyComplex = false;
336        for (Extension ext : list) {
337          anyComplex = anyComplex || ext.hasExtension();
338        }
339        if (!anyComplex) {
340          XhtmlNode tr = tbl.tr();
341          nameCell(tr, getContext().getTranslated(sd.getTitleElement()), sd.getDescription(), sd.getWebPath());
342          XhtmlNode td = tr.td();
343          td.colspan("3");
344          if (list.size() == 1) {
345            XhtmlNode ul = td.ul();
346            for (Extension s : list) {
347              XhtmlNode li = ul.li();
348              render(r, li, s.getValue());
349            }
350          } else {
351            render(r, td, list.get(0).getValue());
352          }
353        } else {
354          for (Extension ext : list) {
355            XhtmlNode tr = tbl.tr();
356            nameCell(tr, sd.getTitle()+":", sd.getDescription());
357            XhtmlNode td = tr.td();
358            td.colspan("3");
359            if (ext.hasExtension()) {
360              XhtmlNode ul = td.ul();
361              for (Extension s : ext.getExtension()) {
362                XhtmlNode li = ul.li();
363                li.tx(s.getUrl()+": ");
364                if (s.hasExtension()) {
365                  boolean first = true;
366                  for (Extension t : s.getExtension()) {
367                    if (first) first = false; else li.tx("; ");
368                    li.tx(t.getUrl()+"=");
369                    render(r, li, t.getValue());
370                  }
371                } else {
372                  render(r, li, s.getValue());
373                }
374              }
375            } else {
376              render(r, td, ext.getValue());
377            }
378          }
379        }
380      }
381    }
382    
383    
384  }
385
386  private void addIdentifiers(XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
387    List<Identifier> ids = new ArrayList<>();
388    PropertyWrapper pw = getProperty(r, "identifier");
389    for (BaseWrapper t : pw.getValues()) {
390      ids.add((Identifier) t.getBase());
391    }
392    Identifier id = null;
393    for (Identifier i : ids) {
394      id = chooseId(id, i);
395    }
396    if (id != null) {
397      ids.remove(id);
398    };
399    if (ids.size() > 0) {
400      XhtmlNode tr = tbl.tr();
401      nameCell(tr, context.formatMessagePlural(ids.size(), RenderingContext.PAT_OTHER_ID),context.formatMessagePlural(ids.size(), RenderingContext.PAT_OTHER_ID_HINT));
402      XhtmlNode td = tr.td();
403      td.colspan("3");
404      if (ids.size() == 1) {
405        render(r, td, ids.get(0));
406      } else { 
407        XhtmlNode ul = td.ul();
408        for (Identifier i : ids) {
409          render(r, ul.li(), i);
410        }
411      }
412    }
413  }
414
415  private void addLangs(XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
416    List<CodeableConcept> langs = new ArrayList<>();
417    PropertyWrapper pw = getProperty(r, "communication");
418    CodeableConcept prefLang = null;
419    for (BaseWrapper t : pw.getValues()) {
420      PropertyWrapper l = getProperty(t, "language");
421      if (l != null && l.hasValues()) {
422        CodeableConcept lang = (CodeableConcept) l.getValues().get(0).getBase();
423        langs.add(lang);
424        l = getProperty(t, "preferred");
425        if (l != null && l.hasValues() && "true".equals(l.getValues().get(0).getBase().primitiveValue())) {
426          prefLang = lang;
427        }
428      }
429    }
430    if (langs.size() > 0) {
431      XhtmlNode tr = tbl.tr();
432      nameCell(tr, context.formatMessagePlural(langs.size(), RenderingContext.PAT_LANG), context.formatMessagePlural(langs.size(), RenderingContext.PAT_LANG_HINT));
433      XhtmlNode td = tr.td();
434      td.colspan("3");
435      if (langs.size() == 1) {
436        render(r, td, langs.get(0));
437        if (prefLang != null) {
438          td.tx(" "+context.formatPhrase(RenderingContext.PAT_LANG_PREFERRED));
439        }
440      } else if (langs.size() > 1) {
441        XhtmlNode ul = td.ul();
442        for (CodeableConcept i : langs) {
443          XhtmlNode li = ul.li();
444          render(r, li, i);
445          if (i == prefLang) {
446            li.tx(" "+context.formatPhrase(RenderingContext.PAT_LANG_PREFERRED));;
447          }
448        }
449      }
450    }
451  }
452
453  private void addLinks(XhtmlNode tbl, ResourceWrapper r) throws UnsupportedEncodingException, FHIRException, IOException {
454    List<NamedReferance> refs = new ArrayList<>();
455    PropertyWrapper pw = getProperty(r, "generalPractitioner");
456    if (pw != null) {
457      for (BaseWrapper t : pw.getValues()) {
458        refs.add(new NamedReferance(context.formatPhrase(RenderingContext.PAT_GP), (Reference) t.getBase(), t));
459      }
460    }
461    pw = getProperty(r, "managingOrganization");
462    if (pw != null) {
463      for (BaseWrapper t : pw.getValues()) {
464        refs.add(new NamedReferance(context.formatPhrase(RenderingContext.PAT_MO), (Reference) t.getBase(), t));
465      }
466    }
467    pw = getProperty(r, "link");
468    for (BaseWrapper t : pw.getValues()) {
469      PropertyWrapper l = getProperty(t, "other");
470      Reference ref = null;
471      BaseWrapper refw = null;
472      for (BaseWrapper v : l.getValues()) {
473        ref = (Reference) v.getBase();
474        refw = v;
475      }
476      String type = null;
477      l = getProperty(t, "type");
478      for (BaseWrapper v : l.getValues()) {
479        type = v.getBase().primitiveValue();
480      }
481      if (type != null && ref != null) {
482        refs.add(new NamedReferance(describeLinkedRecord(type), ref, refw));        
483      }
484    }
485    
486    if (refs.size() > 0) {      
487      XhtmlNode tr = tbl.tr();
488      nameCell(tr, context.formatPhrase(RenderingContext.PAT_LINKS), context.formatPhrase(RenderingContext.PAT_LINKS_HINT));
489      XhtmlNode td = tr.td();
490      td.colspan("3");
491      XhtmlNode ul = td.ul();
492      for (NamedReferance ref : refs) {
493        XhtmlNode li = ul.li();
494        li.tx(ref.getName());
495        li.tx(": ");
496        renderReference(r, li, ref.getReference());        
497      }
498    }
499  }
500
501  private String describeLinkedRecord(String type) {
502    switch (type) {
503    case "replaced-by" : return context.formatPhrase(RenderingContext.PAT_LINK_REPLBY);
504    case "replaces": return context.formatPhrase(RenderingContext.PAT_LINK_REPL);
505    case "refer": return context.formatPhrase(RenderingContext.PAT_LINK_REFER);
506    case "seealso": return context.formatPhrase(RenderingContext.PAT_LINK_SEE);
507    }
508    return "Unknown";
509  }
510
511  private void addNOKs(XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
512    PropertyWrapper pw = getProperty(r, "contact");
513    for (BaseWrapper t : pw.getValues()) {
514      addNOK(tbl,r,  t);
515    }
516  }
517  
518  private void addNOK(XhtmlNode tbl, ResourceWrapper r, BaseWrapper bw) throws FHIRFormatError, DefinitionException, IOException {
519    List<CodeableConcept> rels = new ArrayList<>();
520    HumanName name = null;
521    Address add = null;
522    String gender = null;
523    Period period = null;
524    Reference organization = null;
525    List<ContactPoint> tels = new ArrayList<>();
526
527    PropertyWrapper pw = getProperty(bw, "relationship");
528    for (BaseWrapper t : pw.getValues()) {
529      CodeableConcept v = (CodeableConcept) t.getBase();
530      rels.add(v);
531    }
532        
533    pw = getProperty(bw, "name");
534    if (pw.hasValues()) {
535      name = (HumanName) pw.getValues().get(0).getBase();
536    }
537    pw = getProperty(bw, "telecom");
538    for (BaseWrapper t : pw.getValues()) {
539      ContactPoint v = (ContactPoint) t.getBase();
540      tels.add(v);
541    }
542        
543    pw = getProperty(bw, "address");
544    if (pw.hasValues()) {
545      add = (Address) pw.getValues().get(0).getBase();
546    }
547    
548    pw = getProperty(bw, "gender");
549    if (pw.hasValues()) {
550      gender = context.getTranslatedCode(pw.getValues().get(0).getBase(), "http://hl7.org/fhir/administrative-gender");
551    }
552
553    pw = getProperty(bw, "organization");
554    if (pw.hasValues()) {
555      organization = (Reference) pw.getValues().get(0).getBase();
556    }
557    
558    pw = getProperty(bw, "period");
559    if (pw.hasValues()) {
560      period = (Period) pw.getValues().get(0).getBase();
561    }
562    
563    if (rels.size() < 2 && name == null && add == null && gender == null && period == null && organization == null && tels.size() == 0) {
564      return; // nothing to render 
565    }
566    XhtmlNode tr = tbl.tr();
567    if (rels.size() == 1) {
568      nameCell(tr, (rels.get(0).getCodingFirstRep().hasDisplay() ? rels.get(0).getCodingFirstRep().getDisplay() : display(rels.get(0)))+":",  context.formatPhrase(RenderingContext.PAT_NOM_CONTACT)+" "+display(rels.get(0)));
569    } else {
570      nameCell(tr, context.formatPhrase(RenderingContext.GENERAL_CONTACT), context.formatPhrase(RenderingContext.PAT_NOK_CONTACT_HINT));
571    }
572    XhtmlNode td = tr.td();
573    td.colspan("3");
574    XhtmlNode ul = td.ul();
575    XhtmlNode li;
576    if (name != null) {
577      li = ul.li();
578      render(r, li, name);
579      if (gender != null) {
580        li.tx(" "+"("+gender+")");
581      }
582    } else if (gender != null) {
583      li = ul.li();
584      li.tx(context.formatPhrase(RenderingContext.PAT_GENDER, gender));      
585    }
586    if (rels.size() > 1) {
587      li = ul.li();
588      li.tx(context.formatPhrase(RenderingContext.PAT_RELN));
589      boolean first = true;
590      for (CodeableConcept rel : rels) {
591        if (first) first = false; else li.tx(", ");
592        render(r, li, rel);
593      }      
594    }
595    if (add != null) {
596      render(r, ul.li(), add);
597    }
598    for (ContactPoint cp : tels) {
599      render(r, ul.li(), cp);
600    }
601    if (organization != null) {
602      li = ul.li();
603      li.tx(context.formatPhrase(RenderingContext.PAT_ORG));
604      render(r, li, organization);
605    }
606    if (period != null) {
607      li = ul.li();
608      li.tx(context.formatPhrase(RenderingContext.PAT_PERIOD));
609      render(r, li, period);
610    }
611  }
612
613  private void addNames(XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
614    List<HumanName> names = new ArrayList<>();
615    PropertyWrapper pw = getProperty(r, "name");
616    for (BaseWrapper t : pw.getValues()) {
617      names.add((HumanName) t.getBase());
618    }
619    HumanName name = null;
620    for (HumanName n : names) {
621      name = chooseName(name, n);
622    }
623    if (name != null) {
624      names.remove(name);
625    };
626    if (names.size() == 1) {
627      XhtmlNode tr = tbl.tr();
628      nameCell(tr, context.formatPhrase(RenderingContext.PAT_ALT_NAME), context.formatPhrase(RenderingContext.PAT_ALT_NAME_HINT));
629      XhtmlNode td = tr.td();
630      td.colspan("3");
631      if (names.size() == 1) {
632        render(r, td, names.get(0));
633      } else {
634        XhtmlNode ul = td.ul();
635        for (HumanName n : names) {
636          render(r, ul.li(), n);
637        }
638      }
639    }
640  }
641  
642  private void addComms(XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
643    List<ContactPoint> tels = new ArrayList<>();
644    PropertyWrapper pw = getProperty(r, "telecom");
645    for (BaseWrapper t : pw.getValues()) {
646      tels.add((ContactPoint) t.getBase());
647    }
648    List<Address> adds = new ArrayList<>();
649    pw = getProperty(r, "address");
650    for (BaseWrapper t : pw.getValues()) {
651      adds.add((Address) t.getBase());
652    }
653    if (tels.size() + adds.size() > 0) {
654      XhtmlNode tr = tbl.tr();
655      nameCell(tr, context.formatPhrase(RenderingContext.PAT_CONTACT), context.formatPhrase(RenderingContext.PAT_CONTACT_HINT));
656      XhtmlNode td = tr.td();
657      td.colspan("3");
658      if (tels.size() + adds.size() == 1) {
659        if (adds.isEmpty()) {
660          render(r, td, tels.get(0));
661        } else {
662          render(r, td, adds.get(0));
663        }
664      } else {
665        XhtmlNode ul = td.ul();
666        for (ContactPoint n : tels) {
667          render(r, ul.li(), n);
668        }
669        for (Address n : adds) {
670          render(r, ul.li(), n);
671        }
672      }
673    }
674  }
675  
676  private void addStatus(XhtmlNode tbl, ResourceWrapper r) throws FHIRFormatError, DefinitionException, UnsupportedEncodingException, FHIRException, IOException {
677    // TODO Auto-generated method stub
678    int count = 0;
679    if (r.has("active")) {
680      count++;
681    }
682    if (r.has("deceased")) {
683      count++;
684    }
685    if (r.has("maritalStatus")) {
686      count++;
687    }
688    if (r.has("multipleBirth")) {
689      count++;
690    }
691    if (count > 0) {
692      XhtmlNode tr = tbl.tr();
693      int pos = 0;
694      if (r.has("active")) {
695        PropertyWrapper a = r.getChildByName("active");
696        if (a.hasValues()) {
697          pos++;
698          nameCell(tr, context.formatPhrase(RenderingContext.PAT_ACTIVE), context.formatPhrase(RenderingContext.PAT_ACTIVE_HINT));
699          XhtmlNode td = tr.td();
700          if (pos == count) {
701            td.colspan("3");
702          }
703          render(r, td, (DataType) a.value().getBase());
704        }
705      }      
706      if (r.has("deceased[x]")) {
707        PropertyWrapper a = r.getChildByName("deceased[x]");
708        if (a.hasValues()) {
709          pos++;
710          nameCell(tr, context.formatPhrase(RenderingContext.PAT_DECEASED), context.formatPhrase(RenderingContext.PAT_DECEASED_HINT));
711          XhtmlNode td = tr.td();
712          if (pos == count) {
713            td.colspan("3");
714          }
715          render(r, td, (DataType) a.value().getBase());
716        }
717      }      
718      if (r.has("maritalStatus")) {
719        PropertyWrapper a = r.getChildByName("maritalStatus");
720        if (a.hasValues()) {
721          pos++;
722          if (pos == 3) {
723            tr = tbl.tr();          
724          }
725          nameCell(tr, context.formatPhrase(RenderingContext.PAT_MARITAL), context.formatPhrase(RenderingContext.PAT_MARITAL_HINT));
726          XhtmlNode td = tr.td();
727          if (pos == count) {
728            td.colspan("3");
729          }
730          render(r, td, (DataType) a.value().getBase());
731        }
732      }      
733      if (r.has("multipleBirth[x]")) {
734        PropertyWrapper a = r.getChildByName("multipleBirth[x]");
735        if (a.hasValues()) {
736          pos++;
737          if (pos == 3) {
738            tr = tbl.tr();          
739          }
740          nameCell(tr, context.formatPhrase(RenderingContext.PAT_MUL_BIRTH), context.formatPhrase(RenderingContext.PAT_MUL_BIRTH_HINT));
741          XhtmlNode td = tr.td();
742          if (pos == count) {
743            td.colspan("3");
744          }
745          render(r, td, (DataType) a.value().getBase());
746        }
747      }      
748    }  
749  }
750
751  private void nameCell(XhtmlNode tr, String text, String title) {
752    XhtmlNode td = tr.td();
753    td.setAttribute("title", title);
754    td.tx(text);
755    td.style("background-color: #f3f5da");
756  }
757
758  private void nameCell(XhtmlNode tr, String text, String title, String link) {
759    XhtmlNode td = tr.td();
760    td.setAttribute("title", title);
761    if (link != null) {
762      td.ah(link).tx(text); 
763    } else {
764      td.tx(text);
765    }
766    td.style("background-color: #f3f5da");
767  }
768
769  private void renderPhoto(XhtmlNode td, ResourceWrapper r) throws UnsupportedEncodingException, FHIRException, IOException {
770    if (r.has("photo")) {
771      PropertyWrapper a = r.getChildByName("photo");
772      for (BaseWrapper v : a.getValues()) {
773        Attachment att = (Attachment) v.getBase();
774        if (att.getContentType().startsWith("image/") &&
775            att.getData() != null && (!context.isInlineGraphics() || (att.getData().length > 0 && att.getData().length < MAX_IMAGE_LENGTH))) {
776          String ext = extensionForType(att.getContentType());
777          if (context.isInlineGraphics() || Utilities.noString(context.getDestDir()) || ext == null) {
778            td.img("data:"+att.getContentType()+";base64,"+att.getDataElement().asStringValue(), "patient photo");
779          } else {
780            String n = UUID.randomUUID().toString().toLowerCase()+ext;
781            TextFile.bytesToFile(att.getData(), ManagedFileAccess.file(Utilities.path(context.getDestDir(), n)));
782            context.registerFile(n);
783            td.img(n, context.formatPhrase(RenderingContext.PAT_PHOTO));            
784          }
785          return;
786        } 
787      }
788    }      
789    return;
790  }
791
792  private String extensionForType(String contentType) {
793    if (contentType.equals("image/gif")) {
794      return ".gif";
795    }
796    if (contentType.equals("image/png")) {
797      return ".png";
798    }
799    if (contentType.equals("image/jpeg")) {
800      return ".jpg";
801    }
802    return null;
803  }
804
805  private boolean hasRenderablePhoto(ResourceWrapper r) throws UnsupportedEncodingException, FHIRException, IOException {
806    if (r.has("photo")) {
807      PropertyWrapper a = r.getChildByName("photo");
808      for (BaseWrapper v : a.getValues()) {
809        Attachment att = (Attachment) v.getBase();
810        if (att.hasContentType() && att.getContentType().startsWith("image/") &&
811            att.getData() != null && (!context.isInlineGraphics() || (att.getData().length > 0 && att.getData().length < MAX_IMAGE_LENGTH))) {
812          return true;
813        } 
814      }
815    }      
816    return false;
817  }
818
819  private XhtmlNode makeBanner(XhtmlNode para) {
820    para.style("border: 1px #661aff solid; background-color: #e6e6ff; padding: 10px;");
821    return para;
822  }
823}