001package org.hl7.fhir.r5.renderers;
002
003import java.io.ByteArrayOutputStream;
004import java.io.IOException;
005import java.io.UnsupportedEncodingException;
006import java.nio.charset.Charset;
007import java.util.ArrayList;
008import java.util.HashMap;
009import java.util.List;
010import java.util.Map;
011
012import org.hl7.fhir.exceptions.DefinitionException;
013import org.hl7.fhir.exceptions.FHIRException;
014import org.hl7.fhir.exceptions.FHIRFormatError;
015import org.hl7.fhir.r5.context.ContextUtilities;
016import org.hl7.fhir.r5.model.CanonicalType;
017import org.hl7.fhir.r5.model.Enumerations;
018import org.hl7.fhir.r5.model.ExampleScenario;
019import org.hl7.fhir.r5.model.ExampleScenario.ExampleScenarioActorComponent;
020import org.hl7.fhir.r5.model.ExampleScenario.ExampleScenarioInstanceComponent;
021import org.hl7.fhir.r5.model.ExampleScenario.ExampleScenarioInstanceContainedInstanceComponent;
022import org.hl7.fhir.r5.model.ExampleScenario.ExampleScenarioInstanceVersionComponent;
023import org.hl7.fhir.r5.model.ExampleScenario.ExampleScenarioProcessComponent;
024import org.hl7.fhir.r5.model.ExampleScenario.ExampleScenarioProcessStepAlternativeComponent;
025import org.hl7.fhir.r5.model.ExampleScenario.ExampleScenarioProcessStepComponent;
026import org.hl7.fhir.r5.model.ExampleScenario.ExampleScenarioProcessStepOperationComponent;
027import org.hl7.fhir.r5.model.Resource;
028import org.hl7.fhir.r5.model.StructureDefinition;
029import org.hl7.fhir.r5.renderers.utils.RenderingContext;
030import org.hl7.fhir.r5.renderers.utils.RenderingContext.KnownLinkType;
031import org.hl7.fhir.r5.renderers.utils.ResourceWrapper;
032import org.hl7.fhir.r5.utils.EOperationOutcome;
033import org.hl7.fhir.utilities.Utilities;
034import org.hl7.fhir.utilities.xhtml.XhtmlDocument;
035import org.hl7.fhir.utilities.xhtml.XhtmlNode;
036
037import net.sourceforge.plantuml.FileFormat;
038import net.sourceforge.plantuml.FileFormatOption;
039import net.sourceforge.plantuml.SourceStringReader;
040
041public class ExampleScenarioRenderer extends TerminologyRenderer {
042
043  public ExampleScenarioRenderer(RenderingContext context) { 
044    super(context); 
045  } 
046 
047  @Override
048  public void buildNarrative(RenderingStatus status, XhtmlNode x, ResourceWrapper r) throws FHIRFormatError, DefinitionException, IOException, FHIRException, EOperationOutcome {
049    if (r.isDirect()) {
050      renderResourceTechDetails(r, x);
051      genSummaryTable(status, x, (ExampleScenario) r.getBase());
052      render(status, x, (ExampleScenario) r.getBase(), r);      
053    } else {
054      // the intention is to change this in the future
055      x.para().tx("ExampleScenarioRenderer only renders native resources directly");
056    }
057  }
058  
059  @Override
060  public String buildSummary(ResourceWrapper r) throws UnsupportedEncodingException, IOException {
061    return canonicalTitle(r);
062  }
063
064  public void render(RenderingStatus status, XhtmlNode x, ExampleScenario scen, ResourceWrapper res) throws FHIRException {
065    try {
066      if (context.getScenarioMode() == null) {
067        renderActors(status, res, x, scen);
068      } else {
069        switch (context.getScenarioMode()) {
070        case ACTORS:
071          renderActors(status, res, x, scen);
072          break;
073        case INSTANCES:
074          renderInstances(status, res, x, scen);
075          break;
076        case PROCESSES:
077          renderProcesses(status, x, scen);
078          break;
079        default:
080          throw new FHIRException(context.formatPhrase(RenderingContext.EX_SCEN_UN, context.getScenarioMode()) + " ");
081        }
082      }
083    } catch (Exception e) {
084      e.printStackTrace();
085      throw new FHIRException(context.formatPhrase(RenderingContext.EX_SCEN_ERR_REN, scen.getUrl(), e) + " ");
086    }
087  }
088
089  public String renderDiagram(RenderingStatus status, ResourceWrapper res, ExampleScenario scen) throws IOException {
090    try {
091      String plantUml = toPlantUml(status, res, scen);
092      SourceStringReader reader = new SourceStringReader(plantUml);
093      final ByteArrayOutputStream os = new ByteArrayOutputStream();
094      reader.outputImage(os, new FileFormatOption(FileFormat.SVG));
095      os.close();
096
097      final String svg = new String(os.toByteArray(), Charset.forName("UTF-8"));
098      return svg;
099    } catch (Exception e) {
100      return "<p style=\"color: maroon\"><b>"+Utilities.escapeXml(e.getMessage())+"</b></p>";
101    }
102  }
103
104  protected String toPlantUml(RenderingStatus status, ResourceWrapper res, ExampleScenario scen) throws IOException {
105    String plantUml = "@startuml\r\n";
106    plantUml += "Title " + (scen.hasTitle() ? scen.getTitle() : scen.getName()) + "\r\n\r\n";
107    Map<String, String> actorKeys = new HashMap<String, String>();
108
109    for (ExampleScenarioActorComponent actor: scen.getActor()) {
110      String actorType = actor.getType().equals(Enumerations.ExampleScenarioActorType.PERSON) ? "actor" : "participant";
111      actorKeys.put(actor.getKey(), escapeKey(actor.getKey()));
112      plantUml += actorType + " \"" + creolLink(actor.getTitle(), "#a_" + actor.getKey(), actor.getDescription()) + "\" as " + actorKeys.get(actor.getKey()) + "\r\n";
113    }
114    plantUml += "\r\n";
115
116    int processNum = 1;
117    for (ExampleScenarioProcessComponent process: scen.getProcess()) {
118      plantUml += toPlantUml(status, res, process, Integer.toString(processNum), scen, actorKeys);
119      processNum++;
120    }
121    plantUml += "@enduml";
122
123    return plantUml;
124  }
125
126  private String escapeKey(String origKey) {
127    char[] chars = origKey.toCharArray();
128    for (int i=0; i<chars.length; i++) {
129      char c = chars[i];
130      if (!((c>='A' && c<='Z') || (c>='a' && c<='z') || (c>='0' && c<='9') || c=='@' || c=='.'))
131        chars[i] = '_';
132    }
133    return new String(chars);
134  }
135
136  protected String toPlantUml(RenderingStatus status, ResourceWrapper res, ExampleScenarioProcessComponent process, String prefix, ExampleScenario scen, Map<String, String> actorKeys) throws IOException {
137    String plantUml = "group " + process.getTitle() + " " + creolLink("details", "#p_" + prefix, process.getDescription()) + "\r\n";
138
139    Map<String,Boolean> actorsActive = new HashMap<String, Boolean>();
140    for (ExampleScenarioActorComponent actor : scen.getActor()) {
141      actorsActive.put(actor.getKey(), Boolean.FALSE);
142    }
143    int stepCount = 1;
144    for (ExampleScenarioProcessStepComponent step: process.getStep()) {
145      plantUml += toPlantUml(status, res, step, stepPrefix(prefix, step, stepCount), scen, actorsActive, actorKeys);
146      if (step.getPause())
147        plantUml += context.formatPhrase(RenderingContext.EX_SCEN_TIME)+"\n";
148      stepCount++;
149    }
150
151    plantUml += "end\r\n\r\n";
152    return plantUml;
153  }
154
155  protected String toPlantUml(RenderingStatus status, ResourceWrapper res, ExampleScenarioProcessStepComponent step, String prefix, ExampleScenario scen, Map<String,Boolean> actorsActive, Map<String, String> actorKeys) throws IOException {
156    String plantUml = "";
157    if (step.hasWorkflow()) {
158      XhtmlNode n = new XhtmlDocument();
159      renderCanonical(status, res, n, Resource.class, step.getWorkflowElement());
160      XhtmlNode ref = n.getChildNodes().get(0);
161      plantUml += noteOver(scen.getActor(), context.formatPhrase(RenderingContext.EXAMPLE_SCEN_STEP_SCEN, trimPrefix(prefix), creolLink((ref.getContent()), ref.getAttribute("href"))));
162    } else if (step.hasProcess())
163      plantUml += toPlantUml(status, res, step.getProcess(), prefix, scen, actorKeys);
164    else {
165      // Operation
166      plantUml += toPlantUml(step.getOperation(), prefix, scen, actorsActive, actorKeys);
167    }
168
169    return plantUml;
170  }
171
172  protected String toPlantUml(ExampleScenarioProcessStepOperationComponent op, String prefix, ExampleScenario scen, Map<String,Boolean> actorsActive, Map<String, String> actorKeys) {
173    StringBuilder plantUml = new StringBuilder();
174    plantUml.append(handleActivation(op.getInitiator(), op.getInitiatorActive(), actorsActive, actorKeys));
175    plantUml.append(handleActivation(op.getReceiver(), op.getReceiverActive(), actorsActive, actorKeys));
176    plantUml.append(actorKeys.get(op.getInitiator()) + " -> " + actorKeys.get(op.getReceiver()) + ": ");
177    plantUml.append(creolLink(op.getTitle(), "#s_" + prefix, op.getDescription()));
178    if (op.hasRequest()) {
179      plantUml.append(" (" + creolLink("payload", linkForInstance(op.getRequest())) + ")\r\n");
180    }
181    if (op.hasResponse()) {
182      plantUml.append("activate " + actorKeys.get(op.getReceiver()) + "\r\n");
183      plantUml.append(actorKeys.get(op.getReceiver()) + " --> " + actorKeys.get(op.getInitiator()) + ": ");
184      plantUml.append(creolLink("response", "#s_" + prefix, op.getDescription()));
185      plantUml.append(" (" + creolLink("payload", linkForInstance(op.getResponse())) + ")\r\n");
186      plantUml.append("deactivate " + actorKeys.get(op.getReceiver()) + "\r\n");
187    }
188    plantUml.append(handleDeactivation(op.getInitiator(), op.getInitiatorActive(), actorsActive, actorKeys));
189    plantUml.append(handleDeactivation(op.getReceiver(), op.getReceiverActive(), actorsActive, actorKeys));
190
191    return plantUml.toString();
192  }
193
194  private String handleActivation(String actorId, boolean active, Map<String,Boolean> actorsActive, Map<String, String> actorKeys) {
195    String plantUml = "";
196    Boolean actorWasActive = actorsActive.get(actorId);
197    if (active && !actorWasActive) {
198      plantUml += "activate " + actorKeys.get(actorId) + "\r\n";
199    }
200    return plantUml;
201  }
202
203  private String handleDeactivation(String actorId, boolean active, Map<String,Boolean> actorsActive, Map<String, String> actorKeys) {
204    String plantUml = "";
205    Boolean actorWasActive = actorsActive.get(actorId);
206    if (actorWasActive != null) {
207      if (!active && actorWasActive) {
208        plantUml += "deactivate " + actorKeys.get(actorId) + "\r\n";
209      }
210      if (active != actorWasActive) {
211        actorsActive.remove(actorId);
212        actorsActive.put(actorId, Boolean.valueOf(active));
213      }
214    }
215    return plantUml;
216  }
217
218  private String linkForInstance(ExampleScenarioInstanceContainedInstanceComponent ref) {
219    String plantUml = "#i_" + ref.getInstanceReference();
220    if (ref.hasVersionReference())
221        plantUml += "v_" + ref.getVersionReference();
222    return plantUml;
223  }
224
225  private String trimPrefix(String prefix){
226    return prefix.substring(prefix.lastIndexOf(".") + 1);
227  }
228
229  private String noteOver(List<ExampleScenarioActorComponent> actors, String text) {
230    String plantUml = "Note over ";
231    List actorKeys = new ArrayList<String>();
232    for (ExampleScenarioActorComponent actor: actors) {
233      actorKeys.add(actor.getKey());
234    }
235    plantUml += String.join(", ", actorKeys);
236    plantUml += " " + text;
237    return plantUml;
238  }
239
240  private String creolLink(String text, String url) {
241    return creolLink(text, url, null);
242  }
243
244  private String creolLink(String text, String url, String flyover) {
245    String s = "[[" + url;
246    if (flyover!=null)
247      s += "{" + flyover + "}";
248    s += " " + text + "]]";
249    return s;
250  }
251
252  public boolean renderActors(RenderingStatus status, ResourceWrapper res, XhtmlNode x, ExampleScenario scen) throws IOException {
253    XhtmlNode tbl = x.table("table-striped table-bordered");
254    XhtmlNode thead = tbl.tr();
255    thead.th().addText(context.formatPhrase(RenderingContext.GENERAL_NAME));
256    thead.th().addText(context.formatPhrase(RenderingContext.GENERAL_TYPE));
257    thead.th().addText(context.formatPhrase(RenderingContext.GENERAL_DESC));
258    for (ExampleScenarioActorComponent actor : scen.getActor()) {
259      XhtmlNode tr = tbl.tr();
260      XhtmlNode nameCell = tr.td();
261      nameCell.an(context.prefixAnchor("a_" + actor.getKey()));
262      nameCell.tx(actor.getTitle());
263      tr.td().tx(actor.getType().getDisplay());
264      addMarkdown(tr.td().style("overflow-wrap:break-word"), actor.getDescription());
265    }
266    return true;
267  }
268
269  public boolean renderInstances(RenderingStatus status, ResourceWrapper res, XhtmlNode x, ExampleScenario scen) throws IOException {
270    XhtmlNode tbl = x.table("table-striped table-bordered");
271    XhtmlNode thead = tbl.tr();
272    thead.th().addText(context.formatPhrase(RenderingContext.GENERAL_NAME));
273    thead.th().addText(context.formatPhrase(RenderingContext.GENERAL_TYPE));
274    thead.th().addText(context.formatPhrase(RenderingContext.GENERAL_CONTENT));
275    thead.th().addText(context.formatPhrase(RenderingContext.GENERAL_DESC));
276
277    Map<String, String> instanceNames = new HashMap<String, String>();
278    for (ExampleScenarioInstanceComponent instance : scen.getInstance()) {
279      instanceNames.put("i_" + instance.getKey(), instance.getTitle());
280      if (instance.hasVersion()) {
281        for (ExampleScenarioInstanceVersionComponent version: instance.getVersion()) {
282          instanceNames.put("i_" + instance.getKey() + "v_" + version.getKey(), version.getTitle());
283        }
284      }
285    }
286
287    for (ExampleScenarioInstanceComponent instance : scen.getInstance()) {
288      XhtmlNode row = tbl.tr();
289      XhtmlNode nameCell = row.td();
290      nameCell.an(context.prefixAnchor("i_" + instance.getKey()));
291      nameCell.tx(instance.getTitle());
292      XhtmlNode typeCell = row.td();
293      if (instance.hasVersion())
294        typeCell.attribute("rowSpan", Integer.toString(instance.getVersion().size()+1));
295
296      if (!instance.hasStructureVersion() || instance.getStructureType().getSystem().equals("")) {
297        if (instance.hasStructureVersion())
298          typeCell.tx((context.formatPhrase(RenderingContext.EX_SCEN_FVER, instance.getStructureVersion()) + " ") + " ");
299        if (instance.hasStructureProfileCanonicalType()) {
300          renderCanonical(status, res, typeCell, StructureDefinition.class, instance.getStructureProfileCanonicalType());
301        } else if (instance.hasStructureProfileUriType()) {
302          renderBase(status, typeCell, instance.getStructureProfileUriType());
303        } else {
304          CanonicalType ct = new CanonicalType("http://hl7.org/fhir/StructureDefinition/" + instance.getStructureType().getCode());
305          renderCanonical(status, res, typeCell, StructureDefinition.class, ct);
306        }
307      } else {
308          renderDataType(status, typeCell, wrapWC(res, instance.getStructureVersionElement()));
309          typeCell.tx(" "+(context.formatPhrase(RenderingContext.GENERAL_VER_LOW, instance.getStructureVersion())+" "));
310        if (instance.hasStructureProfile()) {
311          typeCell.tx(" ");
312          if (instance.hasStructureProfileCanonicalType()) {
313            renderCanonical(status, res, typeCell, StructureDefinition.class, instance.getStructureProfileCanonicalType());
314          }
315        }
316      }
317      if (instance.hasContent() && instance.getContent().hasReference()) {
318        // Force end-user mode to avoid ugly references
319        RenderingContext.ResourceRendererMode mode = context.getMode();
320        context.setMode(RenderingContext.ResourceRendererMode.END_USER);
321        renderReference(status, row.td(), wrapWC(res, instance.getContent().copy().setDisplay("here")));
322        context.setMode(mode);
323      } else
324        row.td();
325
326      XhtmlNode descCell = row.td();
327      addMarkdown(descCell, instance.getDescription());
328      if (instance.hasContainedInstance()) {
329        descCell.b().tx(context.formatPhrase(RenderingContext.EX_SCEN_CONTA) + " ");
330        int containedCount = 1;
331        for (ExampleScenarioInstanceContainedInstanceComponent contained: instance.getContainedInstance()) {
332          String key = "i_" + contained.getInstanceReference();
333          if (contained.hasVersionReference())
334            key += "v_" + contained.getVersionReference();
335          String description = instanceNames.get(key);
336          if (description==null)
337            throw new FHIRException("Unable to find contained instance " + key + " under " + instance.getKey());
338          descCell.ah(context.prefixLocalHref("#" + key)).tx(description);
339          containedCount++;
340          if (instance.getContainedInstance().size() > containedCount)
341            descCell.tx(", ");
342        }
343      }
344
345      for (ExampleScenarioInstanceVersionComponent version: instance.getVersion()) {
346        row = tbl.tr();
347        nameCell = row.td().style("padding-left: 10px;");
348        nameCell.an("i_" + instance.getKey() + "v_" + version.getKey());
349        XhtmlNode nameItem = nameCell.ul().li();
350        nameItem.tx(version.getTitle());
351
352        if (version.hasContent() && version.getContent().hasReference()) {
353          // Force end-user mode to avoid ugly references
354          RenderingContext.ResourceRendererMode mode = context.getMode();
355          context.setMode(RenderingContext.ResourceRendererMode.END_USER);
356          renderReference(status, row.td(), wrapWC(res, version.getContent().copy().setDisplay("here")));
357          context.setMode(mode);
358        } else
359          row.td();
360
361        descCell = row.td();
362        addMarkdown(descCell, instance.getDescription());
363      }
364    }
365    return true;
366  }
367
368  public boolean renderProcesses(RenderingStatus status, XhtmlNode x, ExampleScenario scen) throws IOException {
369    Map<String, ExampleScenarioActorComponent> actors = new HashMap<>();
370    for (ExampleScenarioActorComponent actor: scen.getActor()) {
371      actors.put(actor.getKey(), actor);
372    }
373
374    Map<String, ExampleScenarioInstanceComponent> instances = new HashMap<>();
375    for (ExampleScenarioInstanceComponent instance: scen.getInstance()) {
376      instances.put(instance.getKey(), instance);
377    }
378
379    int num = 1;
380    for (ExampleScenarioProcessComponent process : scen.getProcess()) {
381      renderProcess(status, x, process, Integer.toString(num), actors, instances);
382      num++;
383    }
384    return true;
385  }
386
387  public void renderProcess(RenderingStatus status, XhtmlNode x, ExampleScenarioProcessComponent process, String prefix, Map<String, ExampleScenarioActorComponent> actors, Map<String, ExampleScenarioInstanceComponent> instances) throws IOException {
388    XhtmlNode div = x.div();
389    div.an(context.prefixAnchor("p_" + prefix));
390    div.b().tx(context.formatPhrase(RenderingContext.EX_SCEN_PROC, process.getTitle())+" ");
391    if (process.hasDescription())
392      addMarkdown(div, process.getDescription());
393    if (process.hasPreConditions()) {
394      div.para().b().i().tx(context.formatPhrase(RenderingContext.EX_SCEN_PRECON));
395      addMarkdown(div, process.getPreConditions());
396    }
397    if (process.hasPostConditions()) {
398      div.para().b().i().tx(context.formatPhrase(RenderingContext.EX_SCEN_POSTCON));
399      addMarkdown(div, process.getPostConditions());
400    }
401    XhtmlNode tbl = div.table("table-striped table-bordered").style("width:100%");
402    XhtmlNode thead = tbl.tr();
403    thead.th().addText(context.formatPhrase(RenderingContext.EX_SCEN_STEP));
404    thead.th().addText(context.formatPhrase(RenderingContext.GENERAL_NAME));
405    thead.th().addText(context.formatPhrase(RenderingContext.GENERAL_DESC));
406    thead.th().addText(context.formatPhrase(RenderingContext.EX_SCEN_IN));
407    thead.th().addText(context.formatPhrase(RenderingContext.EX_SCEN_REC));
408    thead.th().addText(context.formatPhrase(RenderingContext.GENERAL_REQUEST));
409    thead.th().addText(context.formatPhrase(RenderingContext.EX_SCEN_RES));
410    int stepCount = 1;
411    for (ExampleScenarioProcessStepComponent step: process.getStep()) {
412      renderStep(status, tbl, step, stepPrefix(prefix, step, stepCount), actors, instances);
413      stepCount++;
414    }
415
416    // Now go through the steps again and spit out any child processes
417    stepCount = 1;
418    for (ExampleScenarioProcessStepComponent step: process.getStep()) {
419      stepSubProcesses(status, tbl, step, stepPrefix(prefix, step, stepCount), actors, instances);
420      stepCount++;
421    }
422  }
423
424  private String stepPrefix(String prefix, ExampleScenarioProcessStepComponent step, int stepCount) {
425    String num = step.hasNumber() ? step.getNumber() : Integer.toString(stepCount);
426    return (!prefix.isEmpty() ? prefix + "." : "") + num;
427  }
428
429  private String altStepPrefix(String prefix, ExampleScenarioProcessStepComponent step, int altNum, int stepCount) {
430    return stepPrefix(prefix + "-Alt" + Integer.toString(altNum) + ".", step, stepCount);
431  }
432
433  private void stepSubProcesses(RenderingStatus status, XhtmlNode x, ExampleScenarioProcessStepComponent step, String prefix, Map<String, ExampleScenarioActorComponent> actors, Map<String, ExampleScenarioInstanceComponent> instances) throws IOException {
434    if (step.hasProcess())
435      renderProcess(status, x, step.getProcess(), prefix, actors, instances);
436    if (step.hasAlternative()) {
437      int altNum = 1;
438      for (ExampleScenarioProcessStepAlternativeComponent alt: step.getAlternative()) {
439        int stepCount = 1;
440        for (ExampleScenarioProcessStepComponent altStep: alt.getStep()) {
441          stepSubProcesses(status, x, altStep, altStepPrefix(prefix, altStep, altNum, stepCount), actors, instances);
442          stepCount++;
443        }
444        altNum++;
445      }
446    }
447  }
448
449  private boolean renderStep(RenderingStatus status, XhtmlNode tbl, ExampleScenarioProcessStepComponent step, String stepLabel, Map<String, ExampleScenarioActorComponent> actors, Map<String, ExampleScenarioInstanceComponent> instances) throws IOException {
450    XhtmlNode row = tbl.tr();
451    XhtmlNode prefixCell = row.td();
452    prefixCell.an(context.prefixAnchor("s_" + stepLabel));
453    prefixCell.tx(stepLabel.substring(stepLabel.indexOf(".") + 1));
454    if (step.hasProcess()) {
455      XhtmlNode n = row.td().colspan(6);
456      n.tx(context.formatPhrase(RenderingContext.EX_SCEN_SEE));
457      n.ah(context.prefixLocalHref("#p_" + stepLabel), step.getProcess().getTitle());
458      n.tx(" "+ context.formatPhrase(RenderingContext.EX_SCEN_BEL));
459
460    } else if (step.hasWorkflow()) {
461      XhtmlNode n = row.td().colspan(6);
462      n.tx(context.formatPhrase(RenderingContext.EX_SCEN_OTH));
463      String link = new ContextUtilities(context.getWorker()).getLinkForUrl(context.getLink(KnownLinkType.SPEC), step.getWorkflow());
464      n.ah(context.prefixLocalHref(link), step.getProcess().getTitle());
465
466    } else {
467      // Must be an operation
468      ExampleScenarioProcessStepOperationComponent op = step.getOperation();
469      XhtmlNode name = row.td();
470      name.tx(op.getTitle());
471      if (op.hasType()) {
472        name.tx(" - ");
473        renderCoding(status, name, wrapNC(op.getType()));
474      }
475      XhtmlNode descCell = row.td();
476      addMarkdown(descCell, op.getDescription());
477
478      addActor(row, op.getInitiator(), actors);
479      addActor(row, op.getReceiver(), actors);
480      addInstance(row, op.getRequest(), instances);
481      addInstance(row, op.getResponse(), instances);
482    }
483
484    int altNum = 1;
485    for (ExampleScenarioProcessStepAlternativeComponent alt : step.getAlternative()) {
486      XhtmlNode altHeading = tbl.tr().colspan(7).td();
487      altHeading.para().i().tx(context.formatPhrase(RenderingContext.EX_SCEN_ALT, alt.getTitle())+" ");
488      if (alt.hasDescription())
489        addMarkdown(altHeading, alt.getDescription());
490      int stepCount = 1;
491      for (ExampleScenarioProcessStepComponent subStep : alt.getStep()) {
492        renderStep(status, tbl, subStep, altStepPrefix(stepLabel, step, altNum, stepCount), actors, instances);
493        stepCount++;
494      }
495      altNum++;
496    }
497
498    return true;
499  }
500
501  private void addActor(XhtmlNode row, String actorId, Map<String, ExampleScenarioActorComponent> actors) throws FHIRException {
502    XhtmlNode actorCell = row.td();
503    if (actorId==null)
504      return;
505    ExampleScenarioActorComponent actor = actors.get(actorId);
506    if (actor==null)
507      throw new FHIRException(context.formatPhrase(RenderingContext.EX_SCEN_UN_ACT, actorId)+" ");
508    actorCell.ah("#a_" + actor.getKey(), actor.getDescription()).tx(actor.getTitle());
509  }
510
511  private void addInstance(XhtmlNode row, ExampleScenarioInstanceContainedInstanceComponent instanceRef, Map<String, ExampleScenarioInstanceComponent> instances) {
512    XhtmlNode instanceCell =  row.td();
513    if (instanceRef==null || instanceRef.getInstanceReference()==null)
514      return;
515    ExampleScenarioInstanceComponent instance = instances.get(instanceRef.getInstanceReference());
516    if (instance==null) {
517      instanceCell.b().tx("Bad reference: "+instanceRef.getInstanceReference());
518    } else if (instanceRef.hasVersionReference()) {
519      ExampleScenarioInstanceVersionComponent theVersion = null;
520      for (ExampleScenarioInstanceVersionComponent version: instance.getVersion()) {
521        if (version.getKey().equals(instanceRef.getVersionReference())) {
522          theVersion = version;
523          break;
524        }
525      }
526      if (theVersion==null)
527        throw new FHIRException("Unable to find referenced version " + instanceRef.getVersionReference() + " within instance " + instanceRef.getInstanceReference());
528      instanceCell.ah(context.prefixLocalHref("#i_" + instance.getKey() + "v_"+ theVersion.getKey()) , theVersion.getDescription()).tx(theVersion.getTitle());
529
530    } else
531      instanceCell.ah(context.prefixLocalHref("#i_" + instance.getKey()), instance.getDescription()).tx(instance.getTitle());
532  }
533}