001package org.hl7.fhir.r5.renderers;
002
003import java.io.IOException;
004import java.io.UnsupportedEncodingException;
005import java.util.ArrayList;
006import java.util.List;
007import java.util.UUID;
008
009import org.hl7.fhir.exceptions.FHIRException;
010import org.hl7.fhir.r5.model.CanonicalType;
011import org.hl7.fhir.r5.model.CodeType;
012import org.hl7.fhir.r5.model.CodeableConcept;
013import org.hl7.fhir.r5.model.Coding;
014import org.hl7.fhir.r5.model.DomainResource;
015import org.hl7.fhir.r5.model.Expression;
016import org.hl7.fhir.r5.model.Extension;
017import org.hl7.fhir.r5.model.PrimitiveType;
018import org.hl7.fhir.r5.model.Questionnaire;
019import org.hl7.fhir.r5.model.ValueSet;
020import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
021import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemAnswerOptionComponent;
022import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent;
023import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemEnableWhenComponent;
024import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemInitialComponent;
025import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemType;
026import org.hl7.fhir.r5.model.Resource;
027import org.hl7.fhir.r5.model.StructureDefinition;
028import org.hl7.fhir.r5.renderers.utils.RenderingContext;
029import org.hl7.fhir.r5.terminologies.ValueSetExpander.ValueSetExpansionOutcome;
030import org.hl7.fhir.r5.utils.ToolingExtensions;
031import org.hl7.fhir.utilities.Utilities;
032import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator;
033import org.hl7.fhir.utilities.xhtml.NodeType;
034import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Cell;
035import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Piece;
036import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.Row;
037import org.hl7.fhir.utilities.xhtml.HierarchicalTableGenerator.TableModel;
038import org.hl7.fhir.utilities.xhtml.XhtmlNode;
039
040public class QuestionnaireRenderer extends TerminologyRenderer {
041  public static final String EXT_QUESTIONNAIRE_ITEM_TYPE_ORIGINAL = "http://hl7.org/fhir/tools/StructureDefinition/original-item-type";
042
043  public QuestionnaireRenderer(RenderingContext context) {
044    super(context);
045  }
046  
047  public boolean render(XhtmlNode x, Resource q) throws UnsupportedEncodingException, IOException {
048    return render(x, (Questionnaire) q);
049  }
050  
051  public boolean render(XhtmlNode x, Questionnaire q) throws UnsupportedEncodingException, IOException {
052    switch (context.getQuestionnaireMode()) {
053    case FORM:  return renderForm(x, q);
054    case LINKS: return renderLinks(x, q);
055    case LOGIC: return renderLogic(x, q);
056    case DEFNS: return renderDefns(x, q);
057    case TREE:  return renderTree(x, q);
058    default:
059      throw new Error("Unknown Questionnaire Renderer Mode");
060    }
061  }
062  
063  public boolean renderTree(XhtmlNode x, Questionnaire q) throws UnsupportedEncodingException, IOException {
064    boolean hasFlags = checkForFlags(q.getItem());
065    boolean doOpts = context.getDefinitionsTarget() == null && hasAnyOptions(q.getItem()); 
066
067    if (doOpts) {
068      x.b().tx("Structure");
069    }
070    HierarchicalTableGenerator gen = new HierarchicalTableGenerator(context.getDestDir(), context.isInlineGraphics(), true);
071    TableModel model = gen.new TableModel("qtree="+q.getId(), !forResource);    
072    model.setAlternating(true);
073    model.setDocoImg(context.getSpecificationLink() +"help16.png");
074    model.setDocoRef(context.getSpecificationLink()+"formats.html#table");
075    model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("sd.head", "LinkId"), translate("sd.hint", "The linkId for the item"), null, 0));
076    model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("sd.head", "Text"), translate("sd.hint", "Text for the item"), null, 0));
077    model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("sd.head", "Cardinality"), translate("sd.hint", "Minimum and Maximum # of times the the itemcan appear in the instance"), null, 0));
078    model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("sd.head", "Type"), translate("sd.hint", "The type of the item"), null, 0));
079    if (hasFlags) {
080      model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("sd.head", "Flags"), translate("sd.hint", "Other attributes of the item"), null, 0));
081    }
082    model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("sd.head", "Description & Constraints"), translate("sd.hint", "Additional information about the item"), null, 0));
083
084    boolean hasExt = false;
085    // first we add a root for the questionaire itself
086    Row row = addTreeRoot(gen, model.getRows(), q, hasFlags);
087    for (QuestionnaireItemComponent i : q.getItem()) {
088      hasExt = renderTreeItem(gen, row.getSubRows(), q, i, hasFlags) || hasExt;
089    }
090    XhtmlNode xn = gen.generate(model, context.getLocalPrefix(), 1, null);
091    x.getChildNodes().add(xn);
092    if (doOpts) {
093      renderOptions(q, x);
094    }
095    return hasExt;
096  }
097
098  private void renderOptions(Questionnaire q, XhtmlNode x) {
099    if (hasAnyOptions(q.getItem())) {
100      x.hr();
101      x.para().b().tx("Option Sets");
102      renderOptions(q.getItem(), x);
103    }    
104  }
105
106  private void renderOptions(List<QuestionnaireItemComponent> items, XhtmlNode x) {    
107    for (QuestionnaireItemComponent i : items) {
108      renderItemOptions(x, i);
109      renderOptions(i.getItem(), x);
110    }    
111  }
112
113  public void renderItemOptions(XhtmlNode x, QuestionnaireItemComponent i) {
114    if (i.hasAnswerOption()) {
115      boolean useSelect = false;
116      for (QuestionnaireItemAnswerOptionComponent opt : i.getAnswerOption()) {
117        useSelect = useSelect || opt.getInitialSelected(); 
118      }
119      x.an("opt-item."+i.getLinkId());
120      x.para().b().tx("Answer options for "+i.getLinkId());
121      XhtmlNode ul = x.ul();
122      for (QuestionnaireItemAnswerOptionComponent opt : i.getAnswerOption()) {
123        XhtmlNode li = ul.li();
124        li.style("font-size: 11px");
125        if (useSelect) {
126          if (opt.getInitialSelected()) {
127            li.img("icon-selected.png");
128          } else {
129            li.img("icon-not-selected.png");            
130          }
131        }
132        if (opt.getValue().isPrimitive()) {
133          li.tx(opt.getValue().primitiveValue());
134        } else if (opt.getValue() instanceof Coding) {
135          Coding c = (Coding) opt.getValue(); 
136          String link = c.hasSystem() ? context.getWorker().getLinkForUrl(context.getSpecificationLink(), c.getSystem()) : null;
137          if (link == null) {
138            li.tx(c.getSystem()+"#"+c.getCode());
139          } else {
140            li.ah(link).tx(describeSystem(c.getSystem()));
141            li.tx(": "+c.getCode());              
142          }
143          if (c.hasDisplay()) {
144            li.tx(" (\""+c.getDisplay()+"\")");              
145          }
146        } else {
147          li.tx("??");            
148        }
149      }
150    }
151  }
152
153  private boolean hasAnyOptions(List<QuestionnaireItemComponent> items) {
154    for (QuestionnaireItemComponent i : items) {
155      if (i.hasAnswerOption()) {
156        return true;
157      }
158      if (hasAnyOptions(i.getItem())) {
159        return true;
160      }
161    }
162    return false;
163  }
164
165  private boolean checkForFlags(List<QuestionnaireItemComponent> items) {
166    for (QuestionnaireItemComponent i : items) {
167      if (checkForFlags(i)) {
168        return true;
169      }
170    }
171    return false;
172  }
173
174  private boolean checkForFlags(QuestionnaireItemComponent i) {
175    if (i.getReadOnly()) {
176      return true;
177    }
178    if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) {
179      return true;
180    }
181    if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")) {
182      return true;
183    }
184    if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay")) {
185      return true;
186    }
187    if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) {
188      return true;
189    }
190    if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation")) {
191      return true;
192    }
193    if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory")) {
194      return true;
195    }
196    return checkForFlags(i.getItem());
197  }
198
199  private Row addTreeRoot(HierarchicalTableGenerator gen, List<Row> rows, Questionnaire q, boolean hasFlags) throws IOException {
200    Row r = gen.new Row();
201    rows.add(r);
202
203    r.setIcon("icon_q_root.gif", "QuestionnaireRoot");
204    r.getCells().add(gen.new Cell(null, null, q.getName(), null, null));
205    r.getCells().add(gen.new Cell(null, null, "", null, null));
206    r.getCells().add(gen.new Cell(null, null, "", null, null));
207    r.getCells().add(gen.new Cell(null, null, "Questionnaire", null, null));
208    if (hasFlags) {
209      r.getCells().add(gen.new Cell(null, null, "", null, null));
210    }
211    r.getCells().add(gen.new Cell(null, null, q.getDescription(), null, null));
212    return r;    
213  }
214  
215  private String getSpecLink(String path) {
216    return Utilities.pathURL(context.getSpecificationLink(), path);
217  }
218
219  private String getSDCLink(String path) {
220    return Utilities.pathURL("http://hl7.org/fhir/uv/sdc", path); // for now?
221  }
222
223  private boolean renderTreeItem(HierarchicalTableGenerator gen, List<Row> rows, Questionnaire q, QuestionnaireItemComponent i, boolean hasFlags) throws IOException {
224    Row r = gen.new Row();
225    rows.add(r);
226    boolean hasExt = false;
227
228    r.setIcon("icon-q-"+i.getType().toCode().toLowerCase()+".png", i.getType().getDisplay());
229    r.getCells().add(gen.new Cell(null, context.getDefinitionsTarget() == null ? "" : context.getDefinitionsTarget()+"#item."+i.getLinkId(), i.getLinkId(), null, null));
230    String txt = (i.hasPrefix() ? i.getPrefix() + ". " : "") + i.getText();
231    r.getCells().add(gen.new Cell(null, null, txt, null, null));
232    r.getCells().add(gen.new Cell(null, null, (i.getRequired() ? "1" : "0")+".."+(i.getRepeats() ? "*" : "1"), null, null));
233    if (i.getTypeElement().hasExtension(EXT_QUESTIONNAIRE_ITEM_TYPE_ORIGINAL)) {
234      String t = i.getTypeElement().getExtensionString(EXT_QUESTIONNAIRE_ITEM_TYPE_ORIGINAL);
235      r.getCells().add(gen.new Cell(null, context.getSpecificationLink()+"codesystem-item-type.html#item-type-"+t, t, null, null));
236    } else {
237      r.getCells().add(gen.new Cell(null, context.getSpecificationLink()+"codesystem-item-type.html#item-type-"+i.getType().toCode(), i.getType().toCode(), null, null));
238    }
239
240    if (hasFlags) {
241      // flags:
242      Cell flags = gen.new Cell();
243      r.getCells().add(flags);
244      if (i.getReadOnly()) {
245        flags.addPiece(gen.new Piece(Utilities.pathURL(context.getSpecificationLink(), "questionnaire-definitions.html#Questionnaire.item.readOnly"), null, "Is Readonly").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-readonly.png"))));
246      }
247      if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) {
248        flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-isSubject.html"), null, "Can change the subject of the questionnaire").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-subject.png"))));
249      }
250      if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")) {
251        flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-hidden.html"), null, "Is a hidden item").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-hidden.png"))));
252      }
253      if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay")) {
254        flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-optionalDisplay.html"), null, "Is optional to display").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-optional.png"))));
255      }
256      if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) {
257        flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-observationLinkPeriod"), null, "Is linked to an observation").addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-observation.png"))));
258      }
259      if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation")) {
260        String code = ToolingExtensions.readStringExtension(i,  "http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation");
261        flags.addPiece(gen.new Piece(getSpecLink("extension-questionnaire-choiceorientation.html"), null, "Orientation: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"))));
262      }
263      if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory")) {
264        CodeableConcept cc = i.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValueCodeableConcept();
265        String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category");
266        flags.addPiece(gen.new Piece(getSDCLink("StructureDefinition-sdc-questionnaire-displayCategory"), null, "Category: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"))));
267      }
268    }    
269    Cell defn = gen.new Cell();
270    r.getCells().add(defn);
271
272    if (i.hasMaxLength()) {
273      defn.getPieces().add(gen.new Piece(null, "Max Length: ", null));
274      defn.getPieces().add(gen.new Piece(null, Integer.toString(i.getMaxLength()), null));
275    }
276    if (i.hasDefinition()) {
277      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
278      defn.getPieces().add(gen.new Piece(null, "Definition: ", null));
279      genDefinitionLink(gen, i, defn);      
280    }
281    if (i.hasEnableWhen()) {
282      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
283      Piece p = gen.new Piece(null, "Enable When: ", null);
284      defn.getPieces().add(p);
285      if (i.getEnableWhen().size() == 0) {
286        XhtmlNode x = new XhtmlNode(NodeType.Element, "span");
287        p.getChildren().add(x);
288        renderEnableWhen(x, i.getEnableWhenFirstRep());        
289      } else {
290        XhtmlNode x = new XhtmlNode(NodeType.Element, "ul");
291        p.getChildren().add(x);
292        for (QuestionnaireItemEnableWhenComponent qi : i.getEnableWhen()) {
293          renderEnableWhen(x.li(), qi);
294        }
295      }
296    }
297    if (i.hasAnswerValueSet()) {
298      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
299      defn.getPieces().add(gen.new Piece(null, "Value Set: ", null));
300      if (!Utilities.noString(i.getAnswerValueSet()) && i.getAnswerValueSet().startsWith("#")) {
301        ValueSet vs = (ValueSet) q.getContained(i.getAnswerValueSet().substring(1));
302        if (vs == null) {
303          defn.getPieces().add(gen.new Piece(null, i.getAnswerValueSet(), null));                    
304        } else {
305          defn.getPieces().add(gen.new Piece(vs.getUserString("path"), vs.present(), null));                              
306        }
307      } else {
308        ValueSet vs = context.getWorker().fetchResource(ValueSet.class, i.getAnswerValueSet());
309        if (vs == null  || !vs.hasUserData("path")) {
310          defn.getPieces().add(gen.new Piece(null, i.getAnswerValueSet(), null));                    
311        } else {
312          defn.getPieces().add(gen.new Piece(vs.getUserString("path"), vs.present(), null));                    
313        }             
314      }
315    }
316    if (i.hasAnswerOption()) {
317      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
318      defn.getPieces().add(gen.new Piece(null, "Options: ", null));
319      if (context.getDefinitionsTarget() == null) {
320        // if we don't have a definitions target, we'll add them below. 
321        defn.getPieces().add(gen.new Piece("#opt-item."+i.getLinkId(), Integer.toString(i.getAnswerOption().size())+" "+Utilities.pluralize("option", i.getAnswerOption().size()), null));
322      } else {
323        defn.getPieces().add(gen.new Piece(context.getDefinitionsTarget()+"#item."+i.getLinkId(), Integer.toString(i.getAnswerOption().size())+" "+Utilities.pluralize("option", i.getAnswerOption().size()), null));
324      }
325    }
326    if (i.hasInitial()) {
327      for (QuestionnaireItemInitialComponent v : i.getInitial()) {
328        if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
329        defn.getPieces().add(gen.new Piece(null, "Initial Value: ", null));
330        defn.getPieces().add(gen.new Piece(null, v.getValue().fhirType(), null));
331        defn.getPieces().add(gen.new Piece(null, " = ", null));
332        if (v.getValue().isPrimitive()) {
333          defn.getPieces().add(gen.new Piece(null, v.getValue().primitiveValue(), null));
334        } else {
335          renderCoding(gen, defn.getPieces(), v.getValueCoding());          
336        }
337      }
338    }
339    // still todo
340
341//
342//http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn
343//
344//http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-width
345//http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod
346//http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl
347//http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue
348    
349    if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-contextExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression")) {
350      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
351      defn.getPieces().add(gen.new Piece(null, "Expressions: ", null));
352      Piece p = gen.new Piece("ul");
353      defn.getPieces().add(p);
354      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression")) {
355        addExpression(p, e.getValueExpression(), "Initial Value", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression");
356      }
357      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-contextExpression")) {
358        addExpression(p, e.getValueExpression(), "Context", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-contextExpression");
359      }
360      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext")) {
361        addExpression(p, e.getValueExpression(), "Item Context", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext");
362      }
363      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression")) {
364        addExpression(p, e.getValueExpression(), "Enable When", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression");
365      }
366      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression")) {
367        addExpression(p, e.getValueExpression(), "Calculated Value", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression");
368      }
369      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression")) {
370        addExpression(p, e.getValueExpression(), "Candidates", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression");
371      } 
372    }
373
374    for (QuestionnaireItemComponent c : i.getItem()) {
375      hasExt = renderTreeItem(gen, r.getSubRows(), q, c, hasFlags) || hasExt;
376    }
377    return hasExt;    
378  }
379
380  public void genDefinitionLink(HierarchicalTableGenerator gen, QuestionnaireItemComponent i, Cell defn) {
381    // can we resolve the definition? 
382    String path = null;
383    String d = i.getDefinition();
384    if (d.contains("#")) {
385      path = d.substring(d.indexOf("#")+1);
386      d = d.substring(0, d.indexOf("#"));
387    }
388    StructureDefinition sd = context.getWorker().fetchResource(StructureDefinition.class, d);
389    if (sd != null) {
390      String url = sd.getUserString("path");
391      if (url != null) {
392        defn.getPieces().add(gen.new Piece(url+"#"+path, path, null));          
393      } else {
394        defn.getPieces().add(gen.new Piece(null, i.getDefinition(), null));
395      }
396    } else {
397      defn.getPieces().add(gen.new Piece(null, i.getDefinition(), null));
398    }
399  }
400
401  public void genDefinitionLink(XhtmlNode x, QuestionnaireItemComponent i) {
402    // can we resolve the definition? 
403    String path = null;
404    String d = i.getDefinition();
405    if (d.contains("#")) {
406      path = d.substring(d.indexOf("#")+1);
407      d = d.substring(0, d.indexOf("#"));
408    }
409    StructureDefinition sd = context.getWorker().fetchResource(StructureDefinition.class, d);
410    if (sd != null) {
411      String url = sd.getUserString("path");
412      if (url != null) {
413        x.ah(url+"#"+path).tx(path);          
414      } else {
415        x.tx(i.getDefinition());
416      }
417    } else {
418      x.tx(i.getDefinition());
419    }
420  }
421
422  private void addExpression(Piece p, Expression exp, String label, String url) {
423    XhtmlNode x = new XhtmlNode(NodeType.Element, "li").style("font-size: 11px");
424    p.addHtml(x);
425    x.ah(url).tx(label);
426    x.tx(": ");
427    x.code(exp.getExpression());
428  }
429
430  private boolean renderLogic(XhtmlNode x, Questionnaire q) throws FHIRException, IOException {
431    HierarchicalTableGenerator gen = new HierarchicalTableGenerator(context.getDestDir(), context.isInlineGraphics(), true);
432    TableModel model = gen.new TableModel("qtree="+q.getId(), true);    
433    model.setAlternating(true);
434    model.setDocoImg(context.getSpecificationLink() +"help16.png");
435    model.setDocoRef(context.getSpecificationLink()+"formats.html#table");
436    model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("sd.head", "LinkId"), translate("sd.hint", "The linkId for the item"), null, 0));
437    model.getTitles().add(gen.new Title(null, model.getDocoRef(), translate("sd.head", "Description & Constraints"), translate("sd.hint", "Additional information about the item"), null, 0));
438
439    boolean hasExt = false;
440    if (!q.hasItem()) {
441      gen.emptyRow(model, 2);
442    } else {
443      for (QuestionnaireItemComponent i : q.getItem()) {
444        hasExt = renderLogicItem(gen, model.getRows(), q, i) || hasExt;
445      }
446    }
447    XhtmlNode xn = gen.generate(model, context.getLocalPrefix(), 1, null);
448    x.getChildNodes().add(xn);
449    return hasExt;  
450  }
451
452  private boolean renderLogicItem(HierarchicalTableGenerator gen, List<Row> rows, Questionnaire q, QuestionnaireItemComponent i) throws IOException {
453    Row r = gen.new Row();
454    rows.add(r);
455    boolean hasExt = false;
456
457    r.setIcon("icon-q-"+i.getType().toCode().toLowerCase()+".png", i.getType().getDisplay());
458    r.getCells().add(gen.new Cell(null, context.getDefinitionsTarget() == null ? "" : context.getDefinitionsTarget()+"#item."+i.getLinkId(), i.getLinkId(), null, null));
459    Cell defn = gen.new Cell();
460    r.getCells().add(defn);
461
462    if (i.hasMaxLength()) {
463      defn.getPieces().add(gen.new Piece(null, "Max Length: ", null));
464      defn.getPieces().add(gen.new Piece(null, Integer.toString(i.getMaxLength()), null));
465    }
466    if (i.hasDefinition()) {
467      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
468      defn.getPieces().add(gen.new Piece(null, "Definition: ", null));
469      genDefinitionLink(gen, i, defn);            
470    }
471    if (i.hasEnableWhen()) {
472      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
473      defn.getPieces().add(gen.new Piece(null, "Enable When: ", null));
474      defn.getPieces().add(gen.new Piece(null, "todo", null));      
475    }
476    if (i.hasAnswerValueSet()) {
477      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
478      defn.getPieces().add(gen.new Piece(null, "Value Set: ", null));
479      if (Utilities.noString(i.getAnswerValueSet()) && i.getAnswerValueSet().startsWith("#")) {
480        ValueSet vs = (ValueSet) q.getContained(i.getAnswerValueSet().substring(1));
481        if (vs == null) {
482          defn.getPieces().add(gen.new Piece(null, i.getAnswerValueSet(), null));                    
483        } else {
484          defn.getPieces().add(gen.new Piece(vs.getUserString("path"), vs.present(), null));                              
485        }
486      } else {
487        ValueSet vs = context.getWorker().fetchResource(ValueSet.class, i.getAnswerValueSet());
488        if (vs == null  || !vs.hasUserData("path")) {
489          defn.getPieces().add(gen.new Piece(null, i.getAnswerValueSet(), null));                    
490        } else {
491          defn.getPieces().add(gen.new Piece(vs.getUserString("path"), vs.present(), null));                    
492        }             
493      }
494    }
495    if (i.hasAnswerOption()) {
496      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
497      defn.getPieces().add(gen.new Piece(null, "Options: ", null));
498      defn.getPieces().add(gen.new Piece(context.getDefinitionsTarget()+"#item."+i.getLinkId(), Integer.toString(i.getAnswerOption().size())+" "+Utilities.pluralize("option", i.getAnswerOption().size()), null));            
499    }
500    if (i.hasInitial()) {
501      for (QuestionnaireItemInitialComponent v : i.getInitial()) {
502        if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
503        defn.getPieces().add(gen.new Piece(null, "Initial Value: ", null));
504        defn.getPieces().add(gen.new Piece(null, v.getValue().fhirType(), null));
505        defn.getPieces().add(gen.new Piece(null, " = ", null));
506        if (v.getValue().isPrimitive()) {
507          defn.getPieces().add(gen.new Piece(null, v.getValue().primitiveValue(), null));
508        } else {
509          renderCoding(gen, defn.getPieces(), v.getValueCoding());          
510        }
511      }
512    }
513    // still todo
514
515//
516//http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn
517//
518//http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-width
519//http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod
520//http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl
521//http://hl7.org/fhir/StructureDefinition/questionnaire-sliderStepValue
522    
523    if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-contextExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression")) {
524      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
525      defn.getPieces().add(gen.new Piece(null, "Expressions: ", null));
526      Piece p = gen.new Piece("ul");
527      defn.getPieces().add(p);
528      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression")) {
529        addExpression(p, e.getValueExpression(), "Initial Value", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression");
530      }
531      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-contextExpression")) {
532        addExpression(p, e.getValueExpression(), "Context", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-contextExpression");
533      }
534      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext")) {
535        addExpression(p, e.getValueExpression(), "Item Context", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext");
536      }
537      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression")) {
538        addExpression(p, e.getValueExpression(), "Enable When", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression");
539      }
540      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression")) {
541        addExpression(p, e.getValueExpression(), "Calculated Value", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression");
542      }
543      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression")) {
544        addExpression(p, e.getValueExpression(), "Candidates", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression");
545      } 
546    }
547
548    for (QuestionnaireItemComponent c : i.getItem()) {
549      hasExt = renderLogicItem(gen, r.getSubRows(), q, c) || hasExt;
550    }
551    return hasExt;
552    
553  }
554
555
556  public boolean renderForm(XhtmlNode x, Questionnaire q) throws UnsupportedEncodingException, IOException {
557    boolean hasExt = false;
558    XhtmlNode d = x.div();
559    boolean hasPrefix = false;
560    for (QuestionnaireItemComponent c : q.getItem()) {
561      hasPrefix = hasPrefix || doesItemHavePrefix(c);
562    }
563    int i = 1;
564    for (QuestionnaireItemComponent c : q.getItem()) {
565      hasExt = renderFormItem(d, q, c, hasPrefix ? null : Integer.toString(i), 0) || hasExt;
566      i++;
567    }
568    return hasExt; 
569  }
570
571  private boolean doesItemHavePrefix(QuestionnaireItemComponent i) {
572    if (i.hasPrefix()) {
573      return true;
574    }
575    for (QuestionnaireItemComponent c : i.getItem()) {
576      if (doesItemHavePrefix(c)) {
577        return true;
578      }
579    }
580    return false;
581  }
582
583  private boolean renderFormItem(XhtmlNode x, Questionnaire q, QuestionnaireItemComponent i, String pfx, int indent) throws IOException {
584    boolean hasExt = false;
585    XhtmlNode d = x.div().style("width: "+Integer.toString(900-indent*10)+"px; border-top: 1px #eeeeee solid");
586    if (indent > 0) {
587      d.style("margin-left: "+Integer.toString(10*indent)+"px");
588    }
589    XhtmlNode display = d.div().style("display: inline-block; width: "+Integer.toString(500-indent*10)+"px");
590    XhtmlNode details = d.div().style("border: 1px #ccccff solid; padding: 2px; display: inline-block; background-color: #fefce7; width: 380px");
591    XhtmlNode p = display.para();
592    if (i.getType() == QuestionnaireItemType.GROUP) {
593      p = p.b();
594    }
595    if (i.hasPrefix()) {
596      p.tx(i.getPrefix());
597      p.tx(": ");
598    }
599    p.span(null, "linkId: "+i.getLinkId()).tx(i.getText());
600    if (i.getRequired()) {
601      p.span("color: red", "Mandatory").tx("*");
602    }
603
604    XhtmlNode input = null;
605    switch (i.getType()) {
606    case STRING:
607      p.tx(" ");
608      input = p.input(i.getLinkId(), "text", i.getType().getDisplay(), 60);
609      break;
610    case ATTACHMENT:
611      break;
612    case BOOLEAN:
613      p.tx(" ");
614      input = p.input(i.getLinkId(), "checkbox", i.getType().getDisplay(), 1);
615      break;
616    case CODING:
617      input = p.select(i.getLinkId());
618      listOptions(q, i, input);
619      break;
620    case DATE:
621      p.tx(" ");
622      input = p.input(i.getLinkId(), "date", i.getType().getDisplay(), 10);
623      break;
624    case DATETIME:
625      p.tx(" ");
626      input = p.input(i.getLinkId(), "datetime-local", i.getType().getDisplay(), 25);
627      break;
628    case DECIMAL:
629      p.tx(" ");
630      input = p.input(i.getLinkId(), "number", i.getType().getDisplay(), 15);
631      break;
632    case DISPLAY:
633      break;
634    case GROUP:
635      
636      break;
637    case INTEGER:
638      p.tx(" ");
639      input = p.input(i.getLinkId(), "number", i.getType().getDisplay(), 10);
640      break;
641    case QUANTITY:
642      p.tx(" ");
643      input = p.input(i.getLinkId(), "number", "value", 15);
644      p.tx(" ");
645      input = p.input(i.getLinkId(), "unit", "unit", 10);
646      break;
647    case QUESTION:
648      break;
649    case REFERENCE:
650      break;
651    case TEXT:
652      break;
653    case TIME:
654      break;
655    case URL:
656      break;
657    default:
658      break;
659    }
660    if (input != null) {
661      if (i.getReadOnly()) {
662        input.attribute("readonly", "1");
663        input.style("background-color: #eeeeee");
664      }
665    }
666    
667//  if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation")) {
668//  String code = ToolingExtensions.readStringExtension(i,  "http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation");
669//  flags.addPiece(gen.new Piece("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", null, "Orientation: "+code).addHtml(new XhtmlNode(NodeType.Element, "img").attribute("src", Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"))));
670//}
671
672    
673    XhtmlNode ul = details.ul();
674    boolean hasFlag = false; 
675    XhtmlNode flags = item(ul, "Flags");
676    item(ul, "linkId", i.getLinkId());
677    
678    if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) {
679      hasFlag = true;
680      flags.ah("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject", "Can change the subject of the questionnaire").img(Utilities.path(context.getLocalPrefix(), "icon-qi-subject.png"));
681    }
682    if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")) {
683      hasFlag = true;
684      flags.ah(Utilities.pathURL(context.getSpecificationLink(), "extension-questionnaire-hidden.html"), "Is a hidden item").img(Utilities.path(context.getLocalPrefix(), "icon-qi-hidden.png"));
685      d.style("background-color: #eeeeee");
686    }
687    if (ToolingExtensions.readBoolExtension(i, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay")) {
688      hasFlag = true;
689      flags.ah("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay", "Is optional to display").img(Utilities.path(context.getLocalPrefix(), "icon-qi-optional.png"));
690    }
691    if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) {
692      hasFlag = true;
693      flags.ah("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod", "Is linked to an observation").img(Utilities.path(context.getLocalPrefix(), "icon-qi-observation.png"));
694    }
695    if (i.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory")) {
696      CodeableConcept cc = i.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValueCodeableConcept();
697      String code = cc.getCode("http://hl7.org/fhir/questionnaire-display-category");
698      hasFlag = true;
699      flags.ah("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-displayCategory", "Category: "+code).img(Utilities.path(context.getLocalPrefix(), "icon-qi-"+code+".png"));
700    }
701
702    if (i.hasMaxLength()) {
703      item(ul, "Max Length", Integer.toString(i.getMaxLength()));
704    }
705    if (i.hasDefinition()) {
706      genDefinitionLink(item(ul, "Definition"), i);      
707    }
708    if (i.hasEnableWhen()) {
709      item(ul, "Enable When", "todo");
710    }
711    if (i.hasAnswerValueSet()) {
712      XhtmlNode ans = item(ul, "Answers");
713      if (!Utilities.noString(i.getAnswerValueSet()) && i.getAnswerValueSet().startsWith("#")) {
714        ValueSet vs = (ValueSet) q.getContained(i.getAnswerValueSet().substring(1));
715        if (vs == null) {
716          ans.tx(i.getAnswerValueSet());                    
717        } else {
718          ans.ah(vs.getUserString("path")).tx(vs.present());                              
719        }
720      } else {
721        ValueSet vs = context.getWorker().fetchResource(ValueSet.class, i.getAnswerValueSet());
722        if (vs == null  || !vs.hasUserData("path")) {
723          ans.tx(i.getAnswerValueSet());                    
724        } else {
725          ans.ah(vs.getUserString("path")).tx(vs.present());                              
726        }             
727      }
728    }
729    if (i.hasAnswerOption()) {
730      item(ul, "Answers", Integer.toString(i.getAnswerOption().size())+" "+Utilities.pluralize("option", i.getAnswerOption().size()), context.getDefinitionsTarget()+"#item."+i.getLinkId());
731    }
732    if (i.hasInitial()) {
733      XhtmlNode vi = item(ul, "Initial Values");
734      boolean first = true;
735      for (QuestionnaireItemInitialComponent v : i.getInitial()) {
736        if (first) first = false; else vi.tx(", ");
737        if (v.getValue().isPrimitive()) {
738          vi.tx(v.getValue().primitiveValue());
739        } else {
740          renderCoding(vi, v.getValueCoding(), true);           
741        }
742      }
743    }
744    if (!hasFlag) {
745      ul.remove(flags);
746    }
747//    if (i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-contextExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression") || i.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression")) {
748//      if (!defn.getPieces().isEmpty()) defn.addPiece(gen.new Piece("br"));
749//      defn.getPieces().add(gen.new Piece(null, "Expressions: ", null));
750//      Piece p = gen.new Piece("ul");
751//      defn.getPieces().add(p);
752//      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression")) {
753//        addExpression(p, e.getValueExpression(), "Initial Value", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression");
754//      }
755//      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-contextExpression")) {
756//        addExpression(p, e.getValueExpression(), "Context", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-contextExpression");
757//      }
758//      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext")) {
759//        addExpression(p, e.getValueExpression(), "Item Context", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemContext");
760//      }
761//      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression")) {
762//        addExpression(p, e.getValueExpression(), "Enable When", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression");
763//      }
764//      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression")) {
765//        addExpression(p, e.getValueExpression(), "Calculated Value", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression");
766//      }
767//      for (Extension e : i.getExtensionsByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression")) {
768//        addExpression(p, e.getValueExpression(), "Candidates", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression");
769//      } 
770//    }
771//
772
773    int t = 1;
774    for (QuestionnaireItemComponent c : i.getItem()) {
775      hasExt = renderFormItem(x, q, c, pfx == null ? null : pfx+"."+Integer.toString(t), indent+1) || hasExt;
776      t++;
777    }
778    return hasExt; 
779  }
780
781  private void item(XhtmlNode ul, String name, String value, String valueLink) {
782    if (!Utilities.noString(value)) {
783      ul.li().style("font-size: 10px").ah(valueLink).tx(name+": "+value);
784    }
785  }
786
787  private void item(XhtmlNode ul, String name, String value) {
788    if (!Utilities.noString(value)) {
789      ul.li().style("font-size: 10px").tx(name+": "+value);
790    }
791  }
792  private XhtmlNode item(XhtmlNode ul, String name) {
793    XhtmlNode li = ul.li();
794    li.style("font-size: 10px").tx(name+": ");
795    return li;
796  }
797
798
799  private void listOptions(Questionnaire q, QuestionnaireItemComponent i, XhtmlNode select) {
800    if (i.hasAnswerValueSet()) {
801      ValueSet vs = null;
802      if (!Utilities.noString(i.getAnswerValueSet()) && i.getAnswerValueSet().startsWith("#")) {
803        vs = (ValueSet) q.getContained(i.getAnswerValueSet().substring(1));
804        if (vs != null && !vs.hasUrl()) {
805          vs = vs.copy();
806          vs.setUrl(q.getUrl()+"--"+q.getContained(i.getAnswerValueSet().substring(1)));
807        }
808      } else {
809        vs = context.getContext().fetchResource(ValueSet.class, i.getAnswerValueSet());
810      }
811      if (vs != null) {
812        ValueSetExpansionOutcome exp = context.getContext().expandVS(vs, true, false);
813        if (exp.getValueset() != null) {
814          for (ValueSetExpansionContainsComponent cc : exp.getValueset().getExpansion().getContains()) {
815            select.option(cc.getCode(), cc.hasDisplay() ? cc.getDisplay() : cc.getCode(), false);    
816          }
817          return;
818        }
819      }
820    } else if (i.hasAnswerOption()) {
821      renderItemOptions(select, i); 
822    } 
823    select.option("a", "??", false);    
824  }
825
826  public String display(Resource dr) throws UnsupportedEncodingException, IOException {
827    return display((Questionnaire) dr);
828  }
829
830  public String display(Questionnaire q) throws UnsupportedEncodingException, IOException {
831    return "Questionnaire "+q.present();
832  }
833 
834  private boolean renderLinks(XhtmlNode x, Questionnaire q) {
835    x.para().tx("Try this questionnaire out:");
836    XhtmlNode ul = x.ul();
837    ul.li().ah("http://todo.nlm.gov/path?mode=ig&src="+Utilities.pathURL(context.getSelfLink(), "package.tgz")+"&q="+q.getId()+".json").tx("NLM Forms Library");
838    return false;
839  }
840
841  private boolean renderDefns(XhtmlNode x, Questionnaire q) throws IOException {
842    XhtmlNode tbl = x.table("dict");
843    boolean ext = false;
844    ext = renderRootDefinition(tbl, q, new ArrayList<>()) || ext;
845    for (QuestionnaireItemComponent qi : q.getItem()) {
846      ext = renderDefinition(tbl, q, qi, new ArrayList<>()) || ext;
847    }
848    return ext;
849  }
850
851  private boolean renderRootDefinition(XhtmlNode tbl, Questionnaire q, List<QuestionnaireItemComponent> parents) throws IOException {
852    boolean ext = false;
853    XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent");
854    td.an(q.getId());
855    td.img(Utilities.path(context.getLocalPrefix(), "icon_q_root.gif"));
856    td.tx(" Questionnaire ");
857    td.b().tx(q.getId());
858    
859    // general information
860    defn(tbl, "URL", q.getUrl());
861    defn(tbl, "Version", q.getVersion());
862    defn(tbl, "Name", q.getName());
863    defn(tbl, "Title", q.getTitle());
864    if (q.hasDerivedFrom()) {
865      td = defn(tbl, "Derived From");
866      boolean first = true;
867      for (CanonicalType c : q.getDerivedFrom()) {
868        if (first) first = false; else td.tx(", ");
869        td.tx(c.asStringValue()); // todo: make these a reference
870      }
871    }
872    defn(tbl, "Status", q.getStatus().getDisplay());
873    defn(tbl, "Experimental", q.getExperimental());
874    defn(tbl, "Publication Date", q.getDateElement().primitiveValue());
875    defn(tbl, "Approval Date", q.getApprovalDateElement().primitiveValue());
876    defn(tbl, "Last Review Date", q.getLastReviewDateElement().primitiveValue());
877    if (q.hasEffectivePeriod()) {
878      renderPeriod(defn(tbl, "Effective Period"), q.getEffectivePeriod());
879    }
880    
881    if (q.hasSubjectType()) {
882      td = defn(tbl, "Subject Type");
883      boolean first = true;
884      for (CodeType c : q.getSubjectType()) {
885        if (first) first = false; else td.tx(", ");
886        td.tx(c.asStringValue());
887      }
888    }
889    defn(tbl, "Description", q.getDescription());
890    defn(tbl, "Purpose", q.getPurpose());
891    defn(tbl, "Copyright", q.getCopyright());
892    if (q.hasCode()) {
893      td = defn(tbl, Utilities.pluralize("Code", q.getCode().size()));
894      boolean first = true;
895      for (Coding c : q.getCode()) {
896        if (first) first = false; else td.tx(", ");
897        renderCodingWithDetails(td,  c);
898      }
899    }
900    return false;
901  }
902  
903  private boolean renderDefinition(XhtmlNode tbl, Questionnaire q, QuestionnaireItemComponent qi, List<QuestionnaireItemComponent> parents) throws IOException {
904    boolean ext = false;
905    XhtmlNode td = tbl.tr().td("structure").colspan("2").span(null, null).attribute("class", "self-link-parent");
906    td.an("item."+qi.getLinkId());
907    for (QuestionnaireItemComponent p : parents) {
908      td.ah("#item."+p.getLinkId()).img(Utilities.path(context.getLocalPrefix(), "icon_q_item.png"));
909      td.tx(" > ");
910    }
911    td.img(Utilities.path(context.getLocalPrefix(), "icon_q_item.png"));
912    td.tx(" Item ");
913    td.b().tx(qi.getLinkId());
914    
915    // general information
916    defn(tbl, "Link Id", qi.getLinkId());
917    defn(tbl, "Prefix", qi.getPrefix());
918    defn(tbl, "Text", qi.getText());
919    defn(tbl, "Type", qi.getType().getDisplay());
920    defn(tbl, "Required", qi.getRequired(), true);
921    defn(tbl, "Repeats", qi.getRepeats(), true);
922    defn(tbl, "Read Only", qi.getReadOnly(), false);
923    if (ToolingExtensions.readBoolExtension(qi, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject")) {
924      defn(tbl, "Subject", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-isSubject", "This element changes who the subject of the question is", null);
925    }
926    
927    // content control
928    defn(tbl, "Max Length", qi.getMaxLength());
929    if (qi.hasAnswerValueSet()) {
930      defn(tbl, "Value Set", qi.getDefinition(), context.getWorker().fetchResource(ValueSet.class,  qi.getAnswerValueSet()));
931    }
932    if (qi.hasAnswerOption()) {
933      XhtmlNode tr = tbl.tr();
934      tr.td().tx("Allowed Answers");
935      XhtmlNode ul = tr.td().ul();
936      for (QuestionnaireItemAnswerOptionComponent ans : qi.getAnswerOption()) {
937        XhtmlNode li = ul.li();
938        render(li, ans.getValue());
939        if (ans.getInitialSelected()) {
940          li.tx(" (initially selected)");
941        }
942      }      
943    }
944    if (qi.hasInitial()) {
945      XhtmlNode tr = tbl.tr();
946      tr.td().tx(Utilities.pluralize("Initial Answer", qi.getInitial().size()));
947      if (qi.getInitial().size() == 1) {
948        render(tr.td(), qi.getInitialFirstRep().getValue());
949      } else {
950        XhtmlNode ul = tr.td().ul();
951        for (QuestionnaireItemInitialComponent ans : qi.getInitial()) {
952          XhtmlNode li = ul.li();
953          render(li, ans.getValue());
954        }
955      }      
956    }
957
958    // appearance 
959    if (qi.hasExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory")) {
960      XhtmlNode tr = tbl.tr();
961      tr.td().ah("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").tx("Display Category");
962      render(tr.td(), qi.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory").getValue());
963    }
964    if (ToolingExtensions.readBoolExtension(qi, "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")) {
965      defn(tbl, "Hidden Item", "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory", "This item is a hidden question", null);
966    }
967    if (ToolingExtensions.readBoolExtension(qi, "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay")) {
968      defn(tbl, "Hidden Item", "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-optionalDisplay", "This item is optional to display", null);
969    }
970    
971    // formal definitions
972    if (qi.hasDefinition()) {
973      genDefinitionLink(defn(tbl, "Definition"), qi);
974    }
975      
976    if (qi.hasCode()) {
977      XhtmlNode tr = tbl.tr();
978      tr.td().tx(Utilities.pluralize("Code", qi.getCode().size()));
979      XhtmlNode ul = tr.td().ul();
980      for (Coding c : qi.getCode()) {
981        renderCodingWithDetails(ul.li(), c);
982      }
983    }
984    if (qi.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod")) {
985      XhtmlNode tr = tbl.tr();
986      tr.td().ah("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod").tx("Observation Link Period");
987      render(tr.td(), qi.getExtensionByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationLinkPeriod").getValue());
988    }
989    
990    // dynamic management
991    if (qi.hasEnableWhen()) {
992      XhtmlNode tr = tbl.tr();
993      tr.td().tx("Enable When");
994      td = tr.td();
995      if (qi.getEnableWhen().size() == 1) {
996        renderEnableWhen(td, qi.getEnableWhen().get(0));
997      } else {
998        td.tx(qi.getEnableBehavior().getDisplay()+" are true:");
999        XhtmlNode ul = td.ul();
1000        for (QuestionnaireItemEnableWhenComponent ew : qi.getEnableWhen()) {
1001          renderEnableWhen(ul.li(), ew);
1002        }
1003      }      
1004    }
1005    
1006    
1007    // other stuff
1008    
1009
1010    
1011    List<QuestionnaireItemComponent> curr = new ArrayList<>();
1012    curr.addAll(parents);
1013    curr.add(qi);
1014    for (QuestionnaireItemComponent qic : qi.getItem()) {
1015      ext = renderDefinition(tbl, q, qic, curr) || ext;
1016    }
1017    return ext;
1018  }
1019
1020  private void defn(XhtmlNode tbl, String name, String url, Resource res) throws UnsupportedEncodingException, IOException {
1021    if (res != null && res.hasUserData("path")) {
1022      defn(tbl, "Definition", RendererFactory.factory(res, context).display(res), res.getUserString("path"));
1023    } else if (Utilities.isAbsoluteUrlLinkable(url)) {
1024      defn(tbl, "Definition", url, url);
1025    } {
1026      defn(tbl, "Definition", url);
1027    }
1028 
1029  }
1030
1031  private void renderEnableWhen(XhtmlNode x, QuestionnaireItemEnableWhenComponent ew) {
1032    x.ah("#item."+ew.getQuestion()).tx(ew.getQuestion());
1033    x.tx(" ");
1034    x.tx(ew.getOperator().toCode());
1035    x.tx(" ");
1036    x.tx(display(ew.getAnswer()));
1037  }
1038
1039  private XhtmlNode defn(XhtmlNode tbl, String name) {
1040    XhtmlNode tr = tbl.tr();
1041    tr.td().tx(name);
1042    return tr.td();
1043  }
1044  
1045  private void defn(XhtmlNode tbl, String name, int value) {
1046    if (value > 0) {
1047      XhtmlNode tr = tbl.tr();
1048      tr.td().tx(name);
1049      tr.td().tx(value);
1050    }    
1051  }
1052 
1053  
1054  private void defn(XhtmlNode tbl, String name, boolean value) {
1055    XhtmlNode tr = tbl.tr();
1056    tr.td().tx(name);
1057    tr.td().tx(Boolean.toString(value));
1058  }
1059 
1060  private void defn(XhtmlNode tbl, String name, String value) {
1061    if (!Utilities.noString(value)) {
1062      XhtmlNode tr = tbl.tr();
1063      tr.td().tx(name);
1064      tr.td().tx(value);
1065    }    
1066  }
1067  
1068  private void defn(XhtmlNode tbl, String name, String value, String url) {
1069    if (!Utilities.noString(value)) {
1070      XhtmlNode tr = tbl.tr();
1071      tr.td().tx(name);
1072      tr.td().ah(url).tx(value);
1073    }    
1074  }
1075
1076  private void defn(XhtmlNode tbl, String name, String nurl, String value, String url) {
1077    if (!Utilities.noString(value)) {
1078      XhtmlNode tr = tbl.tr();
1079      tr.td().ah(nurl).tx(name);
1080      if (url != null) {
1081        tr.td().ah(url).tx(value);
1082      } else {
1083        tr.td().tx(value);
1084      }
1085    }    
1086  }
1087
1088  private void defn(XhtmlNode tbl, String name, boolean value, boolean ifFalse) {
1089    if (ifFalse || value) {
1090      XhtmlNode tr = tbl.tr();
1091      tr.td().tx(name);
1092      tr.td().tx(Boolean.toString(value));
1093    }    
1094  }
1095
1096}