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}