001package org.hl7.fhir.r5.renderers.utils;
002
003import java.io.IOException;
004import java.util.ArrayList;
005import java.util.Collections;
006import java.util.Comparator;
007import java.util.List;
008
009import org.apache.commons.lang3.StringUtils;
010import org.hl7.fhir.exceptions.DefinitionException;
011import org.hl7.fhir.exceptions.FHIRFormatError;
012import org.hl7.fhir.r5.context.ExpansionOptions;
013import org.hl7.fhir.r5.extensions.ExtensionDefinitions;
014import org.hl7.fhir.r5.extensions.ExtensionUtilities;
015import org.hl7.fhir.r5.model.CanonicalResource;
016import org.hl7.fhir.r5.model.CanonicalType;
017import org.hl7.fhir.r5.model.CodeableConcept;
018import org.hl7.fhir.r5.model.Coding;
019import org.hl7.fhir.r5.model.DataType;
020import org.hl7.fhir.r5.model.ElementDefinition;
021import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
022import org.hl7.fhir.r5.model.Enumerations.BindingStrength;
023import org.hl7.fhir.r5.model.Extension;
024import org.hl7.fhir.r5.model.Quantity;
025import org.hl7.fhir.r5.model.Resource;
026import org.hl7.fhir.r5.model.StructureDefinition;
027import org.hl7.fhir.r5.model.ValueSet;
028import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
029import org.hl7.fhir.r5.renderers.DataRenderer;
030import org.hl7.fhir.r5.renderers.Renderer.RenderingStatus;
031import org.hl7.fhir.r5.renderers.utils.ElementTable.HintDrivenGroupingEngine;
032import org.hl7.fhir.r5.terminologies.expansion.ValueSetExpansionOutcome;
033
034import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
035import org.hl7.fhir.utilities.Utilities;
036import org.hl7.fhir.utilities.json.model.JsonArray;
037import org.hl7.fhir.utilities.json.model.JsonObject;
038import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
039import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell;
040import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row;
041import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel;
042import org.hl7.fhir.utilities.xhtml.NodeType;
043import org.hl7.fhir.utilities.xhtml.XhtmlNode;
044
045
046@MarkedToMoveToAdjunctPackage
047public class ElementTable {
048
049  public static class ElementTableGrouping {
050
051    private long key;
052    private String name;
053    private int priority;
054
055    public ElementTableGrouping(long key, String name, int priority) {
056      this.key = key;
057      this.name = name;
058      this.priority = priority;
059    }
060
061    public String getName() {
062      return name;
063    }
064
065    public int getPriority() {
066      return priority;
067    }
068
069    public long getKey() {
070      return key;
071    }
072
073  }
074  public enum ElementTableGroupingState {
075    UNKNOWN, DEFINES_GROUP, IN_GROUP
076  }
077  public static abstract class ElementTableGroupingEngine {
078
079    public abstract ElementTableGroupingState groupState(ElementDefinition ed);
080    public abstract ElementTableGrouping getGroup(ElementDefinition ed);
081    
082
083    protected boolean nameMatches(String name, List<String> strings) {
084      for (String s : strings) {
085        if (nameMatches(name, s)) {
086          return true;
087        }
088      }
089      return false;
090    }
091    
092    public boolean nameMatches(String name, String test) {
093      if (test.equals(name)) {
094        return true;
095      }
096      if (test.endsWith("[x]") && name.startsWith(test.substring(0, test.length()-3))) {
097        return true;
098      }
099      return false;
100    }    
101  }
102
103  public static class JsonDrivenGroupingEngine extends ElementTableGroupingEngine {
104
105    private JsonArray groups;
106    
107    public JsonDrivenGroupingEngine(JsonArray groups) {
108      super();
109      this.groups = groups;
110    }
111    
112   public ElementTableGroupingState groupState(ElementDefinition ed) {
113     String name = ed.getName();
114      for (JsonObject o : groups.asJsonObjects()  ) {
115        if (nameMatches(name, o.getStrings("elements"))) {
116          return ElementTableGroupingState.IN_GROUP;
117        }
118      }
119      for (JsonObject o : groups.asJsonObjects()  ) {
120        if (o.asBoolean("all")) {
121          return ElementTableGroupingState.IN_GROUP;
122        }
123      }
124      return ElementTableGroupingState.UNKNOWN;
125    }
126    
127    public ElementTableGrouping getGroup(ElementDefinition ed) {
128      String name = ed.getName();
129      int c = 0;
130      for (JsonObject o : groups.asJsonObjects()  ) {
131        c++;
132        if (nameMatches(name, o.getStrings("elements"))) {
133          return new ElementTableGrouping(c, o.asString("name"), groups.size() - c);
134        }
135      }
136      c = 0;
137      for (JsonObject o : groups.asJsonObjects()  ) {
138        c++;
139        if (o.asBoolean("all")) {
140          return new ElementTableGrouping(c, o.asString("name"), groups.size() - c);
141        }
142      }
143      return null;
144    }
145  }
146
147  public static class HintDrivenGroupingEngine extends ElementTableGroupingEngine {
148
149    private List<ElementDefinition> list;
150    
151    public HintDrivenGroupingEngine(List<ElementDefinition> list) {
152      super();
153      this.list = list;
154    }
155    
156    public ElementTableGroupingState groupState(ElementDefinition ed) {
157      if (ed.hasExtension(ExtensionDefinitions.EXT_PROFILE_VIEW_HINT)) {
158        List<Extension> exl = ed.getExtensionsByUrl(ExtensionDefinitions.EXT_PROFILE_VIEW_HINT);
159        for (Extension ex : exl) {
160          if ("element-view-group".equals(ex.getExtensionString("name")) ) {
161            return ElementTableGroupingState.DEFINES_GROUP;
162          }
163        }
164      }
165      return ElementTableGroupingState.UNKNOWN;
166    }
167    
168    public ElementTableGrouping getGroup(ElementDefinition ed) {
169      if (ed.hasExtension(ExtensionDefinitions.EXT_PROFILE_VIEW_HINT)) {
170        String n = null;
171        int order = 0;
172        List<Extension> exl = ed.getExtensionsByUrl(ExtensionDefinitions.EXT_PROFILE_VIEW_HINT);
173        for (Extension ex : exl) {
174          if ("element-view-group".equals(ex.getExtensionString("name")) ) {
175            n = ex.getExtensionString("value");
176          }
177          if ("element-view-order".equals(ex.getExtensionString("name")) ) {
178            order = ExtensionUtilities.readIntegerExtension(ex, "value", 0);
179          }
180        }
181        if (n != null) {
182          return new ElementTableGrouping(ed.getName().hashCode(), n, order);
183        }
184      }
185      return null;
186    }
187  }
188
189  
190  public static enum TableElementDefinitionType {
191    DEFINITION, COMMENT, REQUIREMENTS;
192  } 
193  
194  public static class TableElementDefinition {
195    private TableElementDefinitionType type;
196    private String markdown;
197    public TableElementDefinition(TableElementDefinitionType type, String markdown) {
198      super();
199      this.type = type;
200      this.markdown = markdown;
201    }
202    public TableElementDefinitionType getType() {
203      return type;
204    }
205    public String getMarkdown() {
206      return markdown;
207    }
208    
209  }
210  
211  public enum TableElementConstraintType {
212    CHOICE, PROFILE, TARGET, BINDING, RANGE, FIXED, PATTERN, MAXLENGTH, CARDINALITY;
213  }
214  
215  public static class TableElementConstraint {
216    private TableElementConstraintType type;
217    private DataType value;
218    private DataType value2;
219    private String path;
220    private BindingStrength strength;
221    private String valueSet;
222    private List<TypeRefComponent> types;
223    private List<CanonicalType> list;
224    private Resource source;
225
226    public static TableElementConstraint makeValue(TableElementConstraintType type, String path, DataType value, Resource source) {
227      TableElementConstraint self = new TableElementConstraint();
228      self.type = type;
229      self.path = path;
230      self.value = value;
231      self.source = source;
232      return self;
233    }
234
235    public static TableElementConstraint makeValueVS(TableElementConstraintType type, String path, DataType value, BindingStrength strength, String valueSet, Resource source) {
236      TableElementConstraint self = new TableElementConstraint();
237      self.type = type;
238      self.path = path;
239      self.value = value;
240      self.strength = strength;
241      self.valueSet = valueSet;
242      self.source = source;
243      return self;
244    }
245
246    public static TableElementConstraint makeRange(TableElementConstraintType type, String path, DataType value, DataType value2, Resource source) {
247      TableElementConstraint self = new TableElementConstraint();
248      self.type = type;
249      self.path = path;
250      self.value = value;
251      self.value2 = value2;
252      self.source = source;
253      return self;
254    }
255
256    public static TableElementConstraint makeBinding(TableElementConstraintType type, String path, BindingStrength strength, String valueSet, Resource source) {
257      TableElementConstraint self = new TableElementConstraint();
258      self.type = type;
259      self.path = path;
260      self.strength = strength;
261      self.valueSet = valueSet;
262      self.source = source;
263      return self;
264    }
265
266    public static TableElementConstraint makeTypes(TableElementConstraintType type, String path, List<TypeRefComponent> types, Resource source) {
267      TableElementConstraint self = new TableElementConstraint();
268      self.type = type;
269      self.path = path;
270      self.types = types;
271      self.source = source;
272      return self;
273    }
274
275    public static TableElementConstraint makeList(TableElementConstraintType type, String path, List<CanonicalType> list, Resource source) {
276      TableElementConstraint self = new TableElementConstraint();
277      self.type = type;
278      self.path = path;
279      self.list = list;
280      self.source = source;
281      return self;
282    }
283
284//    private BindingStrength strength;
285//    private ValueSet vs;
286//    private List<String> profiles;
287//    private List<String> targetProfiles;
288//    private DataType fixed;
289//    private DataType pattern;
290//    private DataType minValue;
291//    private DataType maxValue;
292  }
293  
294
295  public class ConstraintsSorter implements Comparator<TableElementConstraint> {
296
297    @Override
298    public int compare(TableElementConstraint o1, TableElementConstraint o2) {
299      int r = StringUtils.compare(o1.path, o2.path);
300      return r == 0 ? o1.type.compareTo(o2.type) : r;
301    }
302  }
303  
304  public static class TableElementInvariant {
305    private String level;
306    private String human;
307    private String fhirPath;
308    private String other;
309    private String otherFormat;
310  }
311  
312  public static class TableElement {
313    // column 1
314    private String path;
315    private String name;
316    private String min;
317    private String max;
318    private String typeName;
319    private String typeIcon;
320    private String typeLink;
321    private String typeHint;
322    
323    // column 2
324    private List<TableElementDefinition> definitions = new ArrayList<>();
325    
326    // column 3
327    private List<TableElementConstraint> constraints = new ArrayList<>();
328    
329    private List<TableElementInvariant> invariants = new ArrayList<>();
330    
331    private List<TableElement> childElements = new ArrayList<ElementTable.TableElement>();
332
333    public TableElement(String path, String name, String min, String max) {
334      super();
335      this.path = path;
336      this.name = name;
337      this.min = min;
338      this.max = max;
339    }
340
341    public String getPath() {
342      return path;
343    }
344
345    public String getName() {
346      return name;
347    }
348
349    public String getMin() {
350      return min;
351    }
352
353    public String getMax() {
354      return max;
355    }
356    public String getTypeName() {
357      return typeName;
358    }
359    public String getTypeIcon() {
360      return typeIcon;
361    }
362    public String getTypeLink() {
363      return typeLink;
364    }
365    public String getTypeHint() {
366      return typeHint;
367    }
368
369    public List<TableElementDefinition> getDefinitions() {
370      return definitions;
371    }
372
373    public List<TableElementConstraint> getConstraints() {
374      return constraints;
375    }
376
377    public List<TableElementInvariant> getInvariants() {
378      return invariants;
379    }
380
381    public List<TableElement> getChildElements() {
382      return childElements;
383    }
384
385    public TableElement setPath(String path) {
386      this.path = path;
387      return this;
388    }
389
390    public TableElement setName(String name) {
391      this.name = name;
392      return this;
393    }
394
395    public TableElement setMin(String min) {
396      this.min = min;
397      return this;
398    }
399
400    public TableElement setMax(String max) {
401      this.max = max;
402      return this;
403    }
404    
405    public void setType(String name, String link, String hint, String icon) {
406      this.typeName = name;
407      this.typeIcon = icon;
408      this.typeLink = link;
409      this.typeHint = hint;
410    }
411    
412  }
413  
414  public static class TableGroup {
415    private String name;
416    private String documentation;
417    private boolean buildIfEmpty;
418    private String emptyNote;
419    private List<TableElement> elements = new ArrayList<ElementTable.TableElement>();
420    private int priorty;
421    private int counter;
422    private ElementTableGrouping definition;
423    
424    public TableGroup(int counter, ElementTableGrouping definition) {
425      super();
426      this.counter = counter;
427      this.definition = definition;
428      name = definition.getName();
429      priorty = definition.getPriority();
430      buildIfEmpty = false;            
431    }
432
433    public String getName() {
434      return name;
435    }
436
437    public String getDocumentation() {
438      return documentation;
439    }
440
441    public boolean isBuildIfEmpty() {
442      return buildIfEmpty;
443    }
444
445    public String getEmptyNote() {
446      return emptyNote;
447    }
448
449    public List<TableElement> getElements() {
450      return elements;
451    }
452
453    public int getPriorty() {
454      return priorty;
455    }
456
457    public int getCounter() {
458      return counter;
459    }
460
461    public ElementTableGrouping getDefinition() {
462      return definition;
463    }
464    
465    
466  }
467  
468  public static class TableGroupSorter implements Comparator<TableGroup> {
469
470    @Override
471    public int compare(TableGroup o1, TableGroup o2) {
472      if (o1.priorty == o2.priorty) {
473        return Integer.compare(o1.counter, o2.counter);        
474      } else {
475        return Integer.compare(o2.priorty, o1.priorty); // priorty sorts backwards
476      }
477    }
478  }
479  
480  private RenderingContext context;
481  private List<TableGroup> groups;
482  private DataRenderer dr;
483  private boolean replaceCardinality;
484  
485  public ElementTable(RenderingContext context, List<TableGroup> groups, DataRenderer dr, boolean replaceCardinality) {
486    this.context = context;
487    this.groups = groups;
488    this.dr = dr;
489    this.replaceCardinality = replaceCardinality;
490  }
491
492  public void build(HierarchicalTableGenerator gen, TableModel table) throws FHIRFormatError, DefinitionException, IOException {
493    Collections.sort(groups, new TableGroupSorter());
494    table.setBorder(true);
495    table.setShowHeadings(false);
496
497    
498    for (TableGroup grp : groups) {
499      if (grp.getElements().size() > 0 || grp.buildIfEmpty) {
500        renderGroup(gen, table, grp);
501      }
502    }
503    
504  }
505
506  private void renderGroup(HierarchicalTableGenerator gen, TableModel table, TableGroup grp) throws FHIRFormatError, DefinitionException, IOException {
507    Row row = gen.new Row();
508    table.getRows().add(row);
509    Cell cell = gen.new Cell(null, null, grp.getName(), null, null);
510    row.getCells().add(cell);
511    cell.span(3);
512    row.setColor("#dfdfdf");
513    cell.addStyle("vertical-align: middle");
514    cell.addStyle("font-weight: bold");
515    cell.addStyle("font-size: 14px");
516    cell.addStyle("padding-top: 10px");
517    cell.addStyle("padding-bottom: 10px");
518
519    boolean first = true;
520    for (TableElement e : grp.elements) {
521      renderElement(gen, row.getSubRows(), e, first);
522      first = false;
523    }
524  }
525
526  private void renderElement(HierarchicalTableGenerator gen, List<Row> rows, TableElement e, boolean first) throws FHIRFormatError, DefinitionException, IOException {
527    Row row = gen.new Row();
528    rows.add(row);
529    if (!first) {
530      row.setTopLine("silver");
531    }
532    renderElementIdentity(gen, row, e);
533    renderElementDefinition(gen, row, e);
534    renderElementConstraints(gen, row, e);
535
536    
537//    if (e.invariants.size() > 0) {
538//      tr.style("border-bottom: none");
539//      tr = table.tr();
540//      tr.style("border-top: none");
541//      tr.style("border-left: black 1px solid");
542//      tr.style("border-right: black 1px solid");  
543//      renderElementInvariants(tr.td().colspan(3), e);
544//    } 
545//    tr.style("border-bottom: silver 1px solid");
546    
547    for (TableElement child : e.getChildElements()) {
548      renderElement(gen, row.getSubRows(), child, false);
549    }
550  }
551
552  public void renderElementIdentity(HierarchicalTableGenerator gen, Row row, TableElement e) {
553    Cell cell = gen.new Cell();
554    cell.addCellStyle("min-width: 220px");
555    row.getCells().add(cell);
556    cell.setInnerTable(true);
557    cell.addText(e.getName()).addStyle("font-weight: bold");
558    cell.addPiece(gen.new Piece("br"));
559    if (!replaceCardinality) {
560      cell.addText("Cardinality: "+e.min+".."+e.max);  
561    } else if ("1".equals(e.min) && "1".equals(e.max)) {
562      cell.addText("Required");
563    } else if ("0".equals(e.min) && "*".equals(e.max)) {
564      cell.addText("Optional, Repeating");
565    } else if ("0".equals(e.min) && "1".equals(e.max)) {
566      cell.addText("Optional");
567    } else if ("1".equals(e.min) && "*".equals(e.max)) {
568      cell.addText("Repeating");
569    } else {
570      cell.addText("Cardinality: "+e.min+".."+e.max);      
571    }
572    cell.addPiece(gen.new Piece("br"));
573    cell.addImg(e.getTypeIcon(), e.getTypeHint(), e.getTypeLink());
574    cell.addPiece(gen.new Piece(e.getTypeLink(), " "+e.getTypeName(), e.getTypeHint()));      
575  }
576
577  public void renderElementConstraints(HierarchicalTableGenerator gen, Row row, TableElement e) throws FHIRFormatError, DefinitionException, IOException {
578    Cell cell = gen.new Cell();
579    cell.addCellStyle("min-width: 300px");
580    row.getCells().add(cell);
581    XhtmlNode div = new XhtmlNode(NodeType.Element, "div");
582    
583    Collections.sort(e.getConstraints(), new ConstraintsSorter());
584    boolean first = true;
585    for (TableElementConstraint c : e.getConstraints()) {
586      if (first) first= false; else div.br();
587      switch (c.type) {
588      case BINDING:
589        renderBindingConstraint(div, c);
590        break;
591      case CHOICE:
592        renderChoiceConstraint(div, c);
593        break;
594      case FIXED:
595        renderValueConstraint(div, c);
596        break;
597      case MAXLENGTH:
598        renderValueConstraint(div, c);
599        break;
600      case PATTERN:
601        renderValueConstraint(div, c);
602        break;
603      case PROFILE:
604        renderListConstraint(div, c);
605        break;
606      case RANGE:
607        renderRangeConstraint(div, c);
608        break;
609      case TARGET:
610        renderListConstraint(div, c);
611        break;
612      case CARDINALITY:
613        renderCardinalityConstraint(div, c);
614      break;
615      default:
616        break;
617      
618      }
619    }
620    cell.addXhtml(div);
621  }
622
623  private void renderBindingConstraint(XhtmlNode x, TableElementConstraint c) {
624    String name = c.path == null ? "value" : c.path;
625    x.code().tx(name);
626    renderBinding(x, c, " is bound to ");
627  }
628
629  private void renderBinding(XhtmlNode x, TableElementConstraint c, String phrase) {
630    ValueSet vs = context.getContext().findTxResource(ValueSet.class, c.valueSet, null, c.source);
631    if (vs == null) {
632      x.tx(phrase+"an unknown valueset ");
633      x.code().tx(c.valueSet);      
634    } else {
635      x.tx(phrase);
636      x.ah(vs.getWebPath()).tx(vs.present());
637      try {      
638        ValueSetExpansionOutcome exp = context.getContext().expandVS(ExpansionOptions.cacheNoHeirarchy().withLanguage(context.getLocale().getLanguage()), vs);
639        if (!exp.isOk()) {
640          x.span().attribute("title", exp.getError()).tx(" (??)");                  
641        } else if (exp.getValueset().getExpansion().getContains().size() == 1000) {
642          x.tx(" (>1000 codes)");                            
643        } else if (exp.getValueset().getExpansion().getContains().size() > 6) {
644          x.tx(" ("+exp.getValueset().getExpansion().getContains().size()+" codes)");                            
645        } else {
646          x.tx(".  Codes:");
647          XhtmlNode ul = x.ul();
648          for (ValueSetExpansionContainsComponent cc : exp.getValueset().getExpansion().getContains()) {
649
650            String url = cc.hasSystem() && cc.hasCode() ? dr.getLinkForCode(cc.getSystem(), cc.getVersion(), cc.getCode(), c.source) : null;
651            var li = ul.li();
652            li.ahOrNot(url).tx(dr.displayCodeSource(cc.getSystem(), cc.getVersion())+": "+cc.getCode());
653            if (cc.hasDisplay()) {
654              li.tx(" \""+cc.getDisplay()+"\"");
655            } 
656          }
657        }
658      } catch (Exception e) {
659        x.span().attribute("title", e.getMessage()).tx(" (??)");        
660      }
661    }
662  }
663
664  private void renderChoiceConstraint(XhtmlNode x, TableElementConstraint c) {
665    String name = c.path == null ? "value" : c.path;
666    x.code().tx(name);
667    x.tx(" is a choice of:");
668    var ul = x.ul();
669    for (TypeRefComponent tr : c.types) {
670      if (tr.hasProfile()) {
671        for (CanonicalType ct : tr.getProfile()) {
672          StructureDefinition sd = context.getContext().fetchTypeDefinition(ct.primitiveValue());
673          if (sd == null || !sd.hasWebPath()) {
674            ul.li().ah(ct.primitiveValue()).tx(ct.primitiveValue());
675          } else {
676            ul.li().ah(sd.getWebPath()).tx(sd.present());
677          }
678        }
679      } else if (tr.hasTarget()) {
680        StructureDefinition sd = context.getContext().fetchTypeDefinition(tr.getWorkingCode());
681        var li = ul.li();
682        li.ah(sd.getWebPath()).tx(sd.present());
683        li.tx(" pointing to ");
684        renderTypeList(x, tr.getTargetProfile());        
685        
686      } else {
687        StructureDefinition sd = context.getContext().fetchTypeDefinition(tr.getWorkingCode());
688        if (sd == null || !sd.hasWebPath()) {
689          ul.li().code().tx(tr.getWorkingCode());
690        } else {
691          ul.li().ah(sd.getWebPath()).tx(sd.present());
692        }
693      }
694    }
695    
696    
697  }
698
699  private void renderValueConstraint(XhtmlNode x, TableElementConstraint c) throws FHIRFormatError, DefinitionException, IOException {
700    String name = c.path == null ? "value" : c.path;
701    x.code().tx(name);
702    switch (c.type) {
703    case FIXED:
704      x.tx(" is fixed to ");
705      break;
706    case MAXLENGTH:
707      x.tx(" is limited  in length to ");
708
709      break;
710    case PATTERN:
711      if (c.value.isPrimitive()) {
712        x.tx(" is fixed to ");
713      } else {
714        x.tx(" must match ");
715      }
716      break;
717    default:
718      break;
719    }
720    renderValue(x, c.value, c.source);
721    if (c.strength != null && c.valueSet != null) {
722      renderBinding(x, c, " from ");
723    }
724  }
725
726  public void renderValue(XhtmlNode x, DataType v, Resource source) throws IOException {
727    if (v.isPrimitive()) {
728      String s = v.primitiveValue();
729      if (Utilities.isAbsoluteUrl(s)) {
730        Resource res = context.getContext().fetchResource(Resource.class, s);
731        if (res != null && res.hasWebPath()) {
732          x.ah(res.getWebPath()).tx(res instanceof CanonicalResource ? ((CanonicalResource) res).present() : s);                    
733        } else if (Utilities.isAbsoluteUrlLinkable(s)) {
734          x.ah(s).code().tx(s);          
735        } else {
736          x.code().tx(s);
737        }
738      } else {
739        x.code().tx(s);
740      }
741    } else if (v instanceof Quantity) {
742      genQuantity(x, (Quantity) v, source);
743    } else if (v instanceof Coding) {
744      genCoding(x, (Coding) v, source);
745    } else if (v instanceof CodeableConcept) {
746      genCodeableConcept(x, (CodeableConcept) v, source);
747    } else {
748      dr.renderBase(new RenderingStatus(), x, v);
749    }
750  }
751
752  private void genCodeableConcept(XhtmlNode div, CodeableConcept cc, Resource source) {
753    boolean first = true;
754    for (Coding c : cc.getCoding()) {
755      if (first) first = false; else div.tx(",");
756      genCoding(div, c, source);
757    }
758    if (cc.hasText()) {
759      div.code().tx(" \""+cc.getText()+"\"");      
760    }
761    
762  }
763
764  public void genQuantity(XhtmlNode div, Quantity q, Resource source) {
765    String url = q.hasSystem() && q.hasUnit() ? dr.getLinkForCode(q.getSystem(), null, q.getCode(), source) : null;
766    var code = div.code();
767    if (q.hasComparator()) {
768      code.tx(q.getComparator().toCode());
769    }
770    code.tx(q.getValueElement().asStringValue());
771    code.ahOrNot(url).tx(q.getUnit());
772  }
773
774  public void genCoding(XhtmlNode div, Coding c, Resource source) {
775    String url = c.hasSystem() && c.hasCode() ? dr.getLinkForCode(c.getSystem(), c.getVersion(), c.getCode(), source) : null;
776    var code = div.code();
777    code.ahOrNot(url).tx(dr.displayCodeSource(c.getSystem(), c.getVersion())+": "+c.getCode());
778    if (c.hasDisplay()) {
779      code.tx(" \""+c.getDisplay()+"\"");
780    } else {
781      String s = dr.lookupCode(c.getSystem(), c.getVersion(), c.getCode());
782      if (s != null) {
783        div.span().style("opacity: 0.5").tx("(\""+s+"\")");
784      }
785    }
786  }
787
788  private void renderRangeConstraint(XhtmlNode x, TableElementConstraint c) throws IOException {
789    String name = c.path == null ? "value" : c.path;
790    if (c.value != null && c.value2 != null) {
791      x.tx(name + " between ");
792      renderValue(x, c.value, c.source);
793      x.tx(" and " );
794      renderValue(x, c.value2, c.source);
795    } else if (c.value != null) {
796      x.tx(name + " more than ");
797      renderValue(x, c.value, c.source);
798    } else  {
799      x.tx(name + " less than ");
800      renderValue(x, c.value2, c.source);
801    }
802  }
803
804  private void renderCardinalityConstraint(XhtmlNode x, TableElementConstraint c) throws IOException {
805    String name = c.path == null ? "value" : c.path;
806    x.code().tx(name);
807    String min = c.value.primitiveValue();
808    String max = c.value2.primitiveValue(); 
809    if (!replaceCardinality) {
810      x.tx("has cardinality: "+min+".."+max);  
811    } else if ("1".equals(min) && "1".equals(max)) {
812      x.tx("is required");
813    } else if ("0".equals(min) && "*".equals(max)) {
814      x.tx("is Optional and repeats");
815    } else if ("0".equals(min) && "1".equals(max)) {
816      x.tx("is Optional");
817    } else if ("1".equals(min) && "*".equals(max)) {
818      x.tx("repeats");
819    } else {
820      x.tx("has cardinality: "+min+".."+max);      
821    }
822  }
823
824  private void renderListConstraint(XhtmlNode x, TableElementConstraint c) {
825    String name = c.path == null ? "value" : c.path;
826
827    x.code().tx(name);
828    switch (c.type) {
829    case PROFILE:
830      x.tx(" must be ");
831      break;
832    case TARGET:
833      x.tx(" must point to ");
834      break;
835    default:
836      break;
837    }
838    renderTypeList(x, c.list);
839  }
840
841  private void renderTypeList(XhtmlNode x, List<CanonicalType> list) {
842
843    if (list.size() == 1) {
844      x.tx("a ");
845    } else {
846      x.tx("one of ");
847    }
848    boolean first = true;
849    for (int i = 0; i < list.size(); i++) {
850      if (first) {
851        first = false;
852      } else if (i == list.size() - 1) {
853        x.tx(" or " );
854      } else {
855        x.tx(", ");
856      }
857      String s = list.get(i).primitiveValue();
858      Resource res = context.getContext().fetchResource(Resource.class, s);
859      if (res != null && res.hasWebPath()) {
860        x.ah(res.getWebPath()).tx(res instanceof CanonicalResource ? ((CanonicalResource) res).present() : s);
861      } else {
862        x.ah(s).tx(s);
863      }
864    }    
865  }
866
867  public void renderElementDefinition(HierarchicalTableGenerator gen, Row row, TableElement e) {
868    Cell cell = gen.new Cell();    row.getCells().add(cell);
869    for (TableElementDefinition d : e.definitions) {
870      if (d.getType() == TableElementDefinitionType.DEFINITION) {
871        cell.addMarkdown(d.getMarkdown());
872      } else if (d.getType() == TableElementDefinitionType.COMMENT) {
873        cell.addMarkdown("Comment: "+d.getMarkdown(), "font-style: italic");
874      }
875    }
876  }
877
878  private void renderElementInvariants(XhtmlNode td, TableElement e) {
879    XhtmlNode ul = td.ul();
880    for (TableElementInvariant t : e.invariants) {
881      var li = ul.li();
882      li.tx(t.level+": "+t.human);
883      li.tx(" ");
884      li.code().tx(t.other != null ? t.other : t.fhirPath);
885    }
886  
887  }
888
889}