001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.io.UnsupportedEncodingException;
005import java.util.ArrayList;
006import java.util.HashMap;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Map;
010import java.util.Set;
011
012import lombok.extern.slf4j.Slf4j;
013import org.hl7.fhir.exceptions.DefinitionException;
014import org.hl7.fhir.exceptions.FHIRException;
015import org.hl7.fhir.exceptions.FHIRFormatError;
016import org.hl7.fhir.r5.conformance.profile.ProfileUtilities.SourcedChildDefinitions;
017import org.hl7.fhir.r5.context.ContextUtilities;
018import org.hl7.fhir.r5.extensions.ExtensionUtilities;
019import org.hl7.fhir.r5.model.ElementDefinition;
020import org.hl7.fhir.r5.model.Resource;
021import org.hl7.fhir.r5.model.StructureDefinition;
022import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
023import org.hl7.fhir.r5.renderers.utils.RenderingContext;
024import org.hl7.fhir.r5.renderers.utils.ResourceWrapper;
025import org.hl7.fhir.r5.renderers.utils.ResourceWrapper.NamedResourceWrapperList;
026import org.hl7.fhir.r5.utils.EOperationOutcome;
027
028import org.hl7.fhir.r5.utils.xver.XVerExtensionManager.XVerExtensionStatus;
029import org.hl7.fhir.r5.utils.xver.XVerExtensionManagerFactory;
030import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
031import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
032import org.hl7.fhir.utilities.Utilities;
033import org.hl7.fhir.utilities.xhtml.NodeType;
034import org.hl7.fhir.utilities.xhtml.XhtmlNode;
035
036@MarkedToMoveToAdjunctPackage
037@Slf4j
038public class ProfileDrivenRenderer extends ResourceRenderer {
039
040  private Set<String> containedIds = new HashSet<>();
041  
042  public ProfileDrivenRenderer(RenderingContext context) {
043    super(context);
044  }
045
046  @Override
047  public void buildNarrative(RenderingStatus status, XhtmlNode x, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException {
048    renderResourceTechDetails(r, x);
049    try {
050      StructureDefinition sd = context.getContext().fetchTypeDefinition(r.fhirType());
051      if (sd == null) {
052        throw new FHIRException(context.formatPhrase(RenderingContext.PROF_DRIV_FEXCP, r.fhirType())+" ");
053      } else {
054        ElementDefinition ed = sd.getSnapshot().getElement().get(0);
055        containedIds.clear();
056        generateByProfile(status, r, sd, r, ed, context.getProfileUtilities().getChildList(sd, ed), x, r.fhirType(), context.isTechnicalMode(), 0);
057      }
058    } catch (Exception e) {
059      log.debug(context.formatPhrase(RenderingContext.PROF_DRIV_ERR_GEN_NARR) +r.fhirType()+"/"+r.getId()+": "+e.getMessage(), e);
060      x.para().b().style("color: maroon").tx(context.formatPhrase(RenderingContext.PROF_DRIV_EXCP, e.getMessage())+" ");
061    }
062  }
063
064  
065  @Override
066  public String buildSummary(ResourceWrapper res) throws UnsupportedEncodingException, IOException {
067    StructureDefinition profile = getContext().getWorker().fetchTypeDefinition(res.fhirType());
068    if (profile == null)
069      return "unknown resource type " +res.fhirType();
070    else {
071      List<ResourceWrapper> children = res.children();
072      ContextUtilities cu = res.getContextUtilities();
073      for (ResourceWrapper p : children) {
074        if (p.name().equals("title") && cu.isDatatype(p.fhirType()) && !p.isEmpty()) {
075          return res.fhirType()+" "+ displayDataType(p);
076        }
077      }
078      for (ResourceWrapper p : children) {
079        if (p.name().equals("name") && cu.isDatatype(p.fhirType()) && !p.isEmpty()) {
080          return res.fhirType()+" "+ displayDataType(p);
081        }
082      }
083      for (ResourceWrapper p : children) {
084        if (p.name().equals("code") && cu.isDatatype(p.fhirType()) && !p.isEmpty()) {
085          return res.fhirType()+" "+ displayDataType(p);
086        }
087      }
088      switch (res.fhirType()) {
089      case "Binary" : return res.fhirType()+": "+res.primitiveValue("contentType")+" ("+res.primitiveValue("data").length()+" bytes base64)";
090      }
091      return generateResourceSummary(res, profile, profile.getSnapshot().getElementFirstRep(), false, false);
092    }
093  }
094
095  public String generateResourceSummary(ResourceWrapper res, StructureDefinition sd, ElementDefinition ed, boolean showCodeDetails, boolean canLink) throws FHIRException, UnsupportedEncodingException, IOException {
096    if (sd == null)
097      return "unknown resource " +res.fhirType();
098    else {
099      SourcedChildDefinitions childDefs = context.getProfileUtilities().getChildMap(sd, ed, true);
100      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("; ");
101      for (NamedResourceWrapperList p : res.childrenInGroups()) {
102        ElementDefinition pDefn = getElementDefinition(childDefs, p); 
103        if (pDefn != null && !ignoreProperty(p) && !pDefn.getBase().getPath().startsWith("Resource.")) {
104          if (p.getValues().size() > 0 && p.getValues().get(0) != null && pDefn != null && isSimple(pDefn) && includeInSummary(pDefn, p)) {
105            CommaSeparatedStringBuilder b2 = new CommaSeparatedStringBuilder(",");
106            for (ResourceWrapper v : p.getValues()) {
107              b2.append(displayDataType(v));
108            }
109            b.append(formatPhrase(RenderingContext.PROF_DRIV_SUMM_PROP, labelForElement(pDefn), b2.toString()));
110          }
111        }
112      }
113      if (b.length() == 0) {
114        return formatPhrase(RenderingContext.PROF_DRIV_SUMM_NONE, res.fhirType());        
115      } else {
116        return formatPhrase(RenderingContext.PROF_DRIV_SUMM, res.fhirType(), b.toString());
117      }
118    }
119  }
120  
121  private String labelForElement(ElementDefinition pDefn) {
122    return pDefn.getName();
123  }
124
125  private ElementDefinition getElementDefinition(SourcedChildDefinitions childDefs, NamedResourceWrapperList p) {
126    for (ElementDefinition ed : childDefs.getList()) {
127      if (ed.getName().equals(p.getName())) {
128        return ed;
129      }
130    }
131    return null;
132  }
133
134  private boolean ignoreProperty(NamedResourceWrapperList p) {
135    return Utilities.existsInList(p.getName(), "contained");
136  }
137
138  private boolean includeInSummary(ElementDefinition child, NamedResourceWrapperList list) throws UnsupportedEncodingException, FHIRException, IOException {
139    if (child.getName().endsWith("active") && list != null && list.getValues().size() > 0 && "true".equals(list.getValues().get(0).primitiveValue())) {
140      return false;
141    }
142    if (child.getIsModifier())
143      return true;
144    if (child.getMustSupport())
145      return true;
146    if (child.getType().size() == 1) {
147      String t = child.getType().get(0).getWorkingCode();
148      if (t.equals("Address") || t.equals("Contact") || t.equals("Reference") || t.equals("Uri") || t.equals("Url") || t.equals("Canonical"))
149        return false;
150    }
151    return true;
152  }
153  
154  private ElementDefinition getElementDefinition(List<ElementDefinition> elements, String path) {
155    for (ElementDefinition element : elements)
156      if (element.getPath().equals(path))
157        return element;
158    return null;
159  }
160
161  private void renderLeaf(RenderingStatus status, ResourceWrapper res, ResourceWrapper ew, StructureDefinition sd, ElementDefinition defn, XhtmlNode parent, XhtmlNode x, boolean title, boolean showCodeDetails, Map<String, String> displayHints, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome {
162    if (ew == null)
163      return;
164
165    if (context.isShowComments()) {
166      x = renderCommentsSpan(x, ew);
167    }
168    if (Utilities.existsInList(ew.fhirType(), "Extension") || ew.isResource()) {
169      return;
170    } else if (ew.fhirType().equals("ElementDefinition")) {
171      x.tx("todo-bundle");
172    } else if (!renderDataType(status, parent, x, ew)) {
173      // well, we have a cell (x) to render this thing, whatever it is
174      // it's not a data type for which we have a built rendering, so we're going to get a list of it's renderable datatype properties, and render them in a list
175      // SourcedChildDefinitions childDefs = context.getProfileUtilities().getChildMap(sd, defn);
176      boolean first = true;
177      x.tx(" (");
178      for (ResourceWrapper child : ew.children()) {
179//        ElementDefinition childDefn = getElementDefinition(childDefs.getList(), child.name());
180        if (child != null && !"Extension".equals(child.fhirType()) && canRenderDataType(child.fhirType())) {
181          if (first) {
182            first = false;
183          } else {
184            x.tx("; ");
185          }
186          x.tx(context.formatMessage(RenderingContext.GENERAL_DATA_DISPLAY_PROPERTY, child.name(), displayDataType(child)));          
187        }
188      }
189      x.tx(")");
190    }    
191  }
192
193  private XhtmlNode renderCommentsSpan(XhtmlNode x, ResourceWrapper e) {
194    if (e.hasFormatComment()) {      
195      return x.span(null, CommaSeparatedStringBuilder.join("&#10;", e.getFormatCommentsPre()));
196    } else {
197      return x;
198    }
199  }
200  
201//  private boolean displayLeaf(ResourceWrapper res, ResourceWrapper ew, ElementDefinition defn, XhtmlNode x, String name, boolean showCodeDetails, boolean allowLinks) throws FHIRException, UnsupportedEncodingException, IOException {
202//    if (ew == null)
203//      return false;
204//
205//    Map<String, String> displayHints = readDisplayHints(defn);
206//
207//    if (name.endsWith("[x]"))
208//      name = name.substring(0, name.length() - 3);
209//
210//    if (!showCodeDetails && ew.isPrimitive() && isDefault(displayHints, ew)) {
211//      return false;
212//    } else if (Utilities.existsInList(ew.fhirType(), "Extension")) {
213//      return false;
214//    } else {
215//      x.addText(name+": "+ displayDataType(ew));
216//      return true;
217//    }
218//  }
219
220
221
222  private boolean isSimple(ElementDefinition e) {
223    //we can tell if e is a primitive because it has types
224    if (e.getType().isEmpty()) {
225      return false;
226    }
227    if (e.getType().size() == 1 && isBase(e.getType().get(0).getWorkingCode())) {
228      return false;
229    }
230    if (e.getType().size() > 1) {
231      return true;
232    }
233    StructureDefinition sd = context.getWorker().fetchTypeDefinition(e.getTypeFirstRep().getCode());
234    if (sd != null) {
235      if (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE) {
236        return true;
237      }
238      if (sd.getKind() == StructureDefinitionKind.COMPLEXTYPE) {
239        if (Utilities.existsInList(e.getTypeFirstRep().getCode(), "Extension", "CodeableConcept", "Coding", "Annotation", "Identifier", "HumanName", "SampledData", 
240            "Address", "ContactPoint", "ContactDetail", "Timing", "Range", "Quantity", "Ratio", "Period", "Reference")) {
241          return true;
242        }        
243      }
244    }
245    return false;
246  }
247
248  private boolean isBase(String code) {
249    return code != null && (code.equals("Element") || code.equals("BackboneElement"));
250  }
251  
252  private SourcedChildDefinitions getChildrenForPath(StructureDefinition profile, String path) throws DefinitionException {
253    var elements = profile.getSnapshot().getElement();
254    // do we need to do a name reference substitution?
255    for (ElementDefinition e : elements) {
256      if (e.getPath().equals(path) && e.hasContentReference()) {
257        String ref = e.getContentReference();
258        if (ref.contains("#")) {
259          ref = ref.substring(ref.indexOf("#"));
260        }
261        ElementDefinition t = null;
262        // now, resolve the name
263        for (ElementDefinition e1 : elements) {
264          if (ref.equals("#"+e1.getId()))
265            t = e1;
266        }
267        if (t == null)
268          throw new DefinitionException("Unable to resolve content reference "+ref+" trying to resolve "+path);
269        path = t.getPath();
270        break;
271      }
272    }
273
274    ElementDefinition t = null;
275    List<ElementDefinition> results = new ArrayList<ElementDefinition>();
276    for (ElementDefinition e : elements) {
277      if (e.getPath().equals(path)) {
278        t = e; 
279      }
280      if (e.getPath().startsWith(path+".") && !e.getPath().substring(path.length()+1).contains("."))
281        results.add(e);
282    }
283    if (results.isEmpty() && t != null && t.getType().size() == 1) {
284       StructureDefinition tsd = context.getWorker().fetchTypeDefinition(t.getTypeFirstRep().getWorkingCode());
285       return getChildrenForPath(tsd, tsd.getType());
286    }
287    return new SourcedChildDefinitions(profile, results, path);
288  }
289
290  private void generateByProfile(RenderingStatus status, ResourceWrapper res, StructureDefinition profile, ResourceWrapper e, ElementDefinition defn, List<ElementDefinition> children,  XhtmlNode x, String path, boolean showCodeDetails, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome {
291    if (children.isEmpty()) {
292      StructureDefinition sdt = context.getWorker().fetchTypeDefinition(e.fhirType());
293      if (sdt != null && (sdt.getKind() == StructureDefinitionKind.COMPLEXTYPE || sdt.getKind() == StructureDefinitionKind.PRIMITIVETYPE)) {
294        renderLeaf(status, res, e, profile, defn, x, x, false, showCodeDetails, readDisplayHints(defn), indent);
295      } else {
296        // we don't have anything to render?
297      }
298    } else {
299      List<NamedResourceWrapperList> pl = splitExtensions(profile, e.childrenInGroups());
300      for (NamedResourceWrapperList p : pl) {
301        generateForProperty(status, res, profile, children, x, path, showCodeDetails, indent, false, p);
302      }
303      for (NamedResourceWrapperList p : pl) {
304        generateForProperty(status, res, profile, children, x, path, showCodeDetails, indent, true, p);
305      }
306    }
307  }
308
309  private void generateForProperty(RenderingStatus status, ResourceWrapper res, StructureDefinition profile,
310      List<ElementDefinition> children, XhtmlNode x, String path,
311      boolean showCodeDetails, int indent, boolean round2, NamedResourceWrapperList p)
312      throws UnsupportedEncodingException, IOException, EOperationOutcome {
313    if (!p.getValues().isEmpty()) {
314      ElementDefinition child = getElementDefinition(children, path+"."+p.getName());
315      if (child != null) {
316        if (!child.getBase().hasPath() || !child.getBase().getPath().startsWith("Resource.")) {
317          generateElementByProfile(status, res, profile, x, path, showCodeDetails, indent, p, child, round2);
318        }
319      }
320    }
321  }
322
323  public void generateElementByProfile(RenderingStatus status, ResourceWrapper res, StructureDefinition profile, XhtmlNode x, String path,
324      boolean showCodeDetails, int indent, NamedResourceWrapperList p, ElementDefinition child, boolean round2) throws UnsupportedEncodingException, IOException, EOperationOutcome {
325    Map<String, String> displayHints = readDisplayHints(child);
326    if ("DomainResource.contained".equals(child.getBase().getPath())) {
327      if (round2) {
328        for (ResourceWrapper v : p.getValues()) {
329          RenderingContext ctxt = context.forContained();
330          if (v.getResourceWrapper() != null && !RendererFactory.hasSpecificRenderer(v.fhirType())) {
331            x.hr();
332            ResourceRenderer rnd = RendererFactory.factory(v.fhirType(), ctxt);
333            rnd.buildNarrative(status, x.blockquote(), v);
334          }
335        }
336      }
337    } else if (!round2 && !exemptFromRendering(child)) {
338      boolean isExt = isExtension(p);
339      if (isExt) {
340        status.setExtensions(true);
341      }
342      SourcedChildDefinitions grandChildren = getChildrenForPath(profile, path+"."+p.getName());
343      filterGrandChildren(grandChildren.getList(), path+"."+p.getName(), p);
344      if (p.getValues().size() > 0) {
345         if (isSimple(child) && !isExt) {
346           XhtmlNode para = x.isPara() ? para = x : x.para();
347           String name = p.getName();
348           if (name.endsWith("[x]"))
349             name = name.substring(0, name.length() - 3);
350           if (showCodeDetails || !isDefaultValue(displayHints, p.getValues())) {
351
352             markBoilerplate(para.b()).addText(name);
353             para.tx(": ");
354             if (renderAsList(child) && p.getValues().size() > 1) {
355               XhtmlNode list = x.ul();
356               for (ResourceWrapper v : p.getValues())
357                 renderLeaf(status, res, v, profile, child, x, xlinkNarrative(list.li(), v), false, showCodeDetails, displayHints, indent);
358             } else {
359               boolean first = true;
360               for (ResourceWrapper v : p.getValues()) {
361                 if (first) {
362                   first = false;
363                 } else {
364                   para.tx(", ");
365                 }
366                 renderLeaf(status, res, v, profile, child, x, spanIfTracking(para, v), false, showCodeDetails, displayHints, indent);
367               }
368             }
369           }
370        } else if (canDoTable(path, p, grandChildren.getList(), x)) {
371          XhtmlNode xn = new XhtmlNode(NodeType.Element, getHeader());
372          xn.addText(Utilities.capitalize(Utilities.camelCase(Utilities.pluralizeMe(p.getName()))));
373          XhtmlNode tbl = new XhtmlNode(NodeType.Element, "table"); 
374          tbl.setAttribute("class", "grid");
375          XhtmlNode tr = tbl.tr();
376          tr.td().style("display: none").tx("-"); // work around problem with empty table rows
377          boolean add = addColumnHeadings(tr, grandChildren.getList());          
378          for (ResourceWrapper v : p.getValues()) {
379            if (v != null) {
380              tr = tbl.tr();
381              tr.td().style("display: none").tx("*"); // work around problem with empty table rows
382              add = addColumnValues(status, res, tr, profile, grandChildren.getList(), v, showCodeDetails, displayHints, indent) || add;
383            }
384          }
385          if (add) {
386            x.add(xn);
387            x.add(tbl);
388          }
389        } else if (isExtension(p)) {
390          StructureDefinition sd = context.getContext().fetchResource(StructureDefinition.class, p.getUrl());          
391          for (ResourceWrapper v : p.getValues()) {
392            if (v != null) {
393              ResourceWrapper vp = v.child("value");
394              List<ResourceWrapper> ev = v.children("extension");
395              if (vp != null) {
396                XhtmlNode para = x.para();
397                para.b().addText(labelforExtension(sd, p.getUrl()));
398                para.tx(": ");
399                renderLeaf(status, res, vp, profile, child, x, para, false, showCodeDetails, displayHints, indent);
400              } else if (!ev.isEmpty()) {
401                XhtmlNode bq = x.addTag("blockquote");  
402                bq.para().b().addText(labelforExtension(sd, p.getUrl()));
403                // what happens now depends. If all the children are simple extensions, they'll be rendered as properties 
404                boolean allSimple = true;
405                for (ResourceWrapper vv : ev) {
406                  if (!vv.has("value")) {
407                    allSimple = false;
408                  }
409                }
410                if (allSimple) {
411                  XhtmlNode ul = bq.ul();
412                  for (ResourceWrapper vv : ev) {
413                    XhtmlNode li = ul.li();
414                    li.tx(labelForSubExtension(vv.primitiveValue("url"), sd));
415                    li.tx(": ");
416                    renderLeaf(status, res, vv.child("value"), sd, child, x, li, isExt, showCodeDetails, displayHints, indent);
417                  }
418                } else {
419                  for (ResourceWrapper vv : ev) {
420                    StructureDefinition ex = context.getWorker().fetchTypeDefinition("Extension");
421                    SourcedChildDefinitions children = getChildrenForPath(ex, "Extension");
422                    generateByProfile(status, res, ex, vv, child, children.getList(), bq, "Extension", showCodeDetails, indent+1);
423                  }
424                }
425              }
426            }
427          }          
428        } else {
429          for (ResourceWrapper v : p.getValues()) {
430            if (v != null) {
431              XhtmlNode bq = x.addTag("blockquote");
432              bq.para().b().addText(p.getName());
433              generateByProfile(status, res, grandChildren.getSource(), v, child, grandChildren.getList(), bq, grandChildren.getPath(), showCodeDetails, indent+1);
434            }
435          }
436        }
437      }
438    }
439  }
440
441//
442//  private String getGrandChildBase(List<ElementDefinition> grandChildren) {
443//    if (grandChildren == null || grandChildren.isEmpty()) {
444//      return null;
445//    }
446//    String[] path = grandChildren.get(0).getPath().split("\\.");
447//    for (int i = 1; i < grandChildren.size(); i++) {
448//      path = getSharedString(path, grandChildren.get(1).getPath().split("\\."));
449//    }
450//    return CommaSeparatedStringBuilder.join(".", path);
451//  }
452//
453//  private String[] getSharedString(String[] path, String[] path2) {
454//    int m = -1;
455//    for (int i = 0; i < Integer.min(path.length, path2.length); i++) {
456//      if (path[i].equals(path2[i])) {
457//        m = i;
458//      } else {
459//        break;
460//      }
461//    }
462//    return m == -1 ? new String[0] : Arrays.copyOfRange(path, 0, m+1);
463//  }
464
465  private String labelForSubExtension(String url, StructureDefinition sd) {  
466    return url;
467  }
468
469  private String labelforExtension(StructureDefinition sd, String url) {
470    if (sd == null) {
471      return tail(url);
472    } else {
473      return sd.present();
474    }
475  }
476
477  private String getHeader() {
478    int i = 3;
479    while (i <= context.getHeaderLevelContext())
480      i++;
481    if (i > 6)
482      i = 6;
483    return "h"+Integer.toString(i);
484  }
485
486  private List<ResourceWrapper> getValues(String path, NamedResourceWrapperList p, ElementDefinition e) {
487    List<ResourceWrapper> res = new ArrayList<ResourceWrapper>();
488    for (ResourceWrapper v : p.getValues()) {
489      for (ResourceWrapper g : v.children()) {
490        if ((path+"."+p.getName()+"."+g.name()).equals(e.getPath()))
491          res.add(v);
492      }
493    }
494    return res;
495  }
496  
497  private boolean canDoTable(String path, NamedResourceWrapperList p, List<ElementDefinition> grandChildren, XhtmlNode x) {
498    if (isExtension(p)) {
499      return false;
500    }
501    if (x.getName().equals("p")) {
502      return false;
503    }
504    
505    if (grandChildren.size() == 0) {
506      return false;
507    }
508
509    for (ElementDefinition e : grandChildren) {
510      List<ResourceWrapper> values = getValues(path, p, e);
511      if (values.size() > 1 || !isSimple(e) || !canCollapse(e))
512        return false;
513    }
514    return true;
515  }
516
517  public boolean isExtension(NamedResourceWrapperList p) {
518    return p.getUrl() != null;
519  }
520
521
522  private boolean canCollapse(ElementDefinition e) {
523    // we can collapse any data type
524    return !e.getType().isEmpty();
525  }
526  private boolean exemptFromRendering(ElementDefinition child) {
527    if (child == null)
528      return false;
529    if ("DomainResource.text".equals(child.getBase().getPath())) {
530      return true;
531    }
532    if ("Composition.subject".equals(child.getPath())) {
533      return true;
534    }
535    if ("Composition.section".equals(child.getPath())) {
536      return true;
537    }
538    return false;
539  }
540
541  private boolean renderAsList(ElementDefinition child) {
542    if (child.getType().size() == 1) {
543      String t = child.getType().get(0).getWorkingCode();
544      if (t.equals("Address") || t.equals("Reference"))
545        return true;
546    }
547    return false;
548  }
549
550  private boolean addColumnHeadings(XhtmlNode tr, List<ElementDefinition> grandChildren) {
551    boolean b = false;
552    for (ElementDefinition e : grandChildren) {
553      b = true;
554      tr.td().b().addText(Utilities.capitalize(tail(e.getPath())));
555    }
556    return b;
557  }
558
559  private boolean addColumnValues(RenderingStatus status, ResourceWrapper res, XhtmlNode tr, StructureDefinition profile, List<ElementDefinition> grandChildren, ResourceWrapper v, boolean showCodeDetails, Map<String, String> displayHints, int indent) throws FHIRException, UnsupportedEncodingException, IOException, EOperationOutcome {
560    boolean b = false;
561    for (ElementDefinition e : grandChildren) {
562      List<ResourceWrapper> p = v.children(e.getPath().substring(e.getPath().lastIndexOf(".")+1));
563      XhtmlNode td = tr.td();
564      if (p == null || p.size() == 0) {
565        b = true;
566        td.tx(" ");
567      } else {
568        for (ResourceWrapper vv : p) {
569          b = true;
570          td.sep(", ");
571          renderLeaf(status, res, vv, profile, e, td, td, false, showCodeDetails, displayHints, indent);
572        }
573      }
574    }
575    return b;
576  }
577
578  private void filterGrandChildren(List<ElementDefinition> grandChildren,  String string, NamedResourceWrapperList prop) {
579    List<ElementDefinition> toRemove = new ArrayList<ElementDefinition>();
580    toRemove.addAll(grandChildren);
581    for (ResourceWrapper b : prop.getValues()) {
582      List<ElementDefinition> list = new ArrayList<ElementDefinition>();
583      for (ElementDefinition ed : toRemove) {
584        List<ResourceWrapper> p = b.children(tail(ed.getPath()));
585        if (p != null && !p.isEmpty())
586          list.add(ed);
587      }
588      toRemove.removeAll(list);
589    }
590    grandChildren.removeAll(toRemove);
591  }
592
593  private List<NamedResourceWrapperList> splitExtensions(StructureDefinition profile, List<NamedResourceWrapperList> children) throws UnsupportedEncodingException, IOException, FHIRException {
594    List<NamedResourceWrapperList> results = new ArrayList<NamedResourceWrapperList>();
595    for (NamedResourceWrapperList p : children) {
596      if (p.getName().equals("extension") || p.getName().equals("modifierExtension")) {
597        // we're going to split these up, and create a property for each url
598        for (ResourceWrapper v : p.getValues()) {
599          String url = v.primitiveValue("url");
600          if (url != null) {
601            // 1. check extension is valid
602            StructureDefinition ed = getContext().getWorker().fetchResource(StructureDefinition.class, url);
603            if (ed == null) {
604              if (xverManager == null) {
605                xverManager = XVerExtensionManagerFactory.createExtensionManager(context.getWorker());
606              }
607              if (xverManager.matchingUrl(url) && xverManager.status(url) == XVerExtensionStatus.Valid) {
608                ed = xverManager.getDefinition(url);
609                new ContextUtilities(getContext().getWorker()).generateSnapshot(ed);
610                getContext().getWorker().getManager().cacheResource(ed);
611              } 
612            }
613            if (p.getName().equals("modifierExtension") && ed == null) {
614              throw new DefinitionException("Unknown modifier extension "+url);
615            } else {
616              // nothing
617            }
618
619            // 2. Park it
620            NamedResourceWrapperList nl = null;
621            for (NamedResourceWrapperList t : results) {
622              if (t.getUrl() != null && t.getUrl().equals(url)) {
623                nl = t;
624              }
625            }
626            if (nl == null) {
627              nl = new NamedResourceWrapperList(p.getName(), url);
628              results.add(nl);
629            }
630            nl.getValues().add(v);
631          }
632        }          
633      } else {
634        results.add(p);
635      }
636    }
637    return results;
638  }
639
640
641  private Map<String, String> readDisplayHints(ElementDefinition defn) throws DefinitionException {
642    Map<String, String> hints = new HashMap<String, String>();
643    if (defn != null) {
644      String displayHint = ExtensionUtilities.getDisplayHint(defn);
645      if (!Utilities.noString(displayHint)) {
646        String[] list = displayHint.split(";");
647        for (String item : list) {
648          String[] parts = item.split(":");
649          if (parts.length == 1) {
650            hints.put("value", parts[0].trim());            
651          } else {
652            if (parts.length != 2) {
653              throw new DefinitionException("error reading display hint: '"+displayHint+"'");
654            }
655            hints.put(parts[0].trim(), parts[1].trim());
656          }
657        }
658      }
659    }
660    return hints;
661  }
662
663  @SuppressWarnings("rawtypes")
664  private boolean isDefaultValue(Map<String, String> displayHints, List<ResourceWrapper> list) throws UnsupportedEncodingException, IOException, FHIRException {
665    if (list.size() != 1)
666      return false;
667    if (list.get(0).isPrimitive())
668      return isDefault(displayHints, list.get(0));
669    else
670      return false;
671  }
672
673  private boolean isDefault(Map<String, String> displayHints, ResourceWrapper primitiveType) {
674    String v = primitiveType.primitiveValue();
675    if (!Utilities.noString(v) && displayHints.containsKey("default") && v.equals(displayHints.get("default")))
676      return true;
677    return false;
678  }
679
680
681  protected String tail(String path) {
682    return path.substring(path.lastIndexOf(".")+1);
683  }
684
685  protected String utail(String path) {
686    return path.contains("/") ? path.substring(path.lastIndexOf("/")+1) : path;
687  }
688
689  public boolean canRender(Resource resource) {
690    return context.getWorker().getResourceNames().contains(resource.fhirType());
691  }
692
693  public RendererType getRendererType() {
694    return RendererType.PROFILE;
695  }
696
697}