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