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