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