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}