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