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