
001package org.hl7.fhir.r5.comparison; 002 003import java.io.File; 004import java.io.IOException; 005import java.io.PrintWriter; 006import java.io.StringWriter; 007import java.util.ArrayList; 008import java.util.Collections; 009import java.util.HashMap; 010import java.util.List; 011import java.util.Map; 012import java.util.Set; 013 014import lombok.extern.slf4j.Slf4j; 015import org.hl7.fhir.exceptions.DefinitionException; 016import org.hl7.fhir.exceptions.FHIRException; 017import org.hl7.fhir.exceptions.FHIRFormatError; 018import org.hl7.fhir.exceptions.PathEngineException; 019import org.hl7.fhir.r5.comparison.CapabilityStatementComparer.CapabilityStatementComparison; 020import org.hl7.fhir.r5.comparison.CodeSystemComparer.CodeSystemComparison; 021import org.hl7.fhir.r5.comparison.ResourceComparer.PlaceHolderComparison; 022import org.hl7.fhir.r5.comparison.ResourceComparer.ResourceComparison; 023import org.hl7.fhir.r5.comparison.StructureDefinitionComparer.ProfileComparison; 024import org.hl7.fhir.r5.comparison.ValueSetComparer.ValueSetComparison; 025import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; 026import org.hl7.fhir.r5.context.IWorkerContext; 027import org.hl7.fhir.r5.fhirpath.FHIRPathEngine; 028import org.hl7.fhir.r5.fhirpath.TypeDetails; 029import org.hl7.fhir.r5.fhirpath.ExpressionNode.CollectionStatus; 030import org.hl7.fhir.r5.fhirpath.IHostApplicationServices; 031import org.hl7.fhir.r5.fhirpath.FHIRPathUtilityClasses.FunctionDetails; 032import org.hl7.fhir.r5.formats.IParser.OutputStyle; 033import org.hl7.fhir.r5.liquid.LiquidEngine; 034import org.hl7.fhir.r5.liquid.LiquidEngine.LiquidDocument; 035import org.hl7.fhir.r5.model.Base; 036import org.hl7.fhir.r5.model.StringType; 037import org.hl7.fhir.r5.model.Tuple; 038import org.hl7.fhir.r5.model.ValueSet; 039import org.hl7.fhir.r5.utils.EOperationOutcome; 040import org.hl7.fhir.utilities.FhirPublication; 041import org.hl7.fhir.utilities.FileUtilities; 042import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage; 043import org.hl7.fhir.utilities.Utilities; 044import org.hl7.fhir.utilities.fhirpath.FHIRPathConstantEvaluationMode; 045import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 046import org.hl7.fhir.utilities.xhtml.XhtmlComposer; 047 048@MarkedToMoveToAdjunctPackage 049@Slf4j 050public class ComparisonRenderer implements IHostApplicationServices { 051 052 private IWorkerContext contextLeft; 053 private IWorkerContext contextRight; 054 private ComparisonSession session; 055 private Map<String, String> templates = new HashMap<>(); 056 private String folder; 057 private String preamble; 058 059 public ComparisonRenderer(IWorkerContext contextLeft, IWorkerContext contextRight, String folder, ComparisonSession session) { 060 super(); 061 this.contextLeft = contextLeft; 062 this.contextRight = contextRight; 063 this.folder = folder; 064 this.session = session; 065 } 066 067 public String getPreamble() { 068 return preamble; 069 } 070 071 public void setPreamble(String preamble) { 072 this.preamble = preamble; 073 } 074 075 public Map<String, String> getTemplates() { 076 return templates; 077 } 078 079 public File render(String leftName, String rightName) throws IOException { 080 dumpBinaries(); 081 StringBuilder b = new StringBuilder(); 082 if (preamble != null) { 083 b.append(preamble); 084 } 085 b.append("<table class=\"grid\">\r\n"); 086 b.append(" <tr>\r\n"); 087 b.append(" <td width=\"260\"><b>"+Utilities.escapeXml(leftName)+"</b></td>\r\n"); 088 b.append(" <td width=\"260\"><b>"+Utilities.escapeXml(rightName)+"</b></td>\r\n"); 089 b.append(" <td width=\"100\"><b>Difference</b></td>\r\n"); 090 b.append(" <td width=\"100\"><b>Union</b></td>\r\n"); 091 b.append(" <td width=\"100\"><b>Intersection</b></td>\r\n"); 092 b.append(" <td width=\"260\"><b>Notes</b></td>\r\n"); 093 b.append(" </tr>\r\n"); 094 095 List<String> list = sorted(session.getCompares().keySet()); 096 processList(list, b, "CodeSystem"); 097 processList(list, b, "ValueSet"); 098 processList(list, b, "StructureDefinition"); 099 processList(list, b, "CapabilityStatement"); 100 b.append("</table>\r\n"); 101 102 Map<String, Base> vars = new HashMap<>(); 103 vars.put("title", new StringType(session.getTitle())); 104 vars.put("list", new StringType(b.toString())); 105 String template = templates.get("Index"); 106 String cnt = processTemplate(template, "CodeSystem", vars); 107 FileUtilities.stringToFile(cnt, file("index.html")); 108 return ManagedFileAccess.file(file("index.html")); 109 } 110 111 private void processList(List<String> list, StringBuilder b, String name) throws IOException { 112 boolean first = true; 113 for (String id : list) { 114 ResourceComparison comp = session.getCompares().get(id); 115 if (comp.fhirType().equals(name)) { 116 if (first) { 117 first = false; 118 b.append("<tr><td colspan=\"6\"><b>"+Utilities.pluralize(name, 2)+"</b></td></tr>\r\n"); 119 } 120 try { 121 renderComparison(id, comp); 122 } catch (Exception e) { 123 log.error("Exception rendering "+id+": "+e.getMessage(), e); 124 } 125 b.append(comp.toTable()); 126 //"<li><a href=\""+comp.getId()+".html\">"+Utilities.escapeXml(comp.summary())+"</a></li>\r\n" 127 } 128 } 129 } 130 131 private List<String> sorted(Set<String> keySet) { 132 List<String> list = new ArrayList<>(); 133 list.addAll(keySet); 134 Collections.sort(list); 135 return list; 136 } 137 138 private void dumpBinaries() throws IOException { 139 if (contextLeft != null && contextLeft.getBinaryKeysAsSet() != null) { 140 for (String k : contextLeft.getBinaryKeysAsSet()) { 141 if (!Utilities.isProhibitedBinaryFile(k)) { 142 FileUtilities.bytesToFile(contextLeft.getBinaryForKey(k), Utilities.path(folder, k)); 143 } 144 } 145 } 146 if (contextRight != null && contextRight.getBinaryKeysAsSet() != null) { 147 for (String k : contextRight.getBinaryKeysAsSet()) { 148 if (!Utilities.isProhibitedBinaryFile(k)) { 149 FileUtilities.bytesToFile(contextRight.getBinaryForKey(k), Utilities.path(folder, k)); 150 } 151 } 152 } 153 } 154 155 private void renderComparison(String id, ResourceComparison comp) throws IOException, FHIRFormatError, DefinitionException, FHIRException, EOperationOutcome { 156 if (comp instanceof ProfileComparison) { 157 renderProfile(id, (ProfileComparison) comp); 158 } else if (comp instanceof ValueSetComparison) { 159 renderValueSet(id, (ValueSetComparison) comp); 160 } else if (comp instanceof CodeSystemComparison) { 161 renderCodeSystem(id, (CodeSystemComparison) comp); 162 } else if (comp instanceof CapabilityStatementComparison) { 163 renderCapabilityStatement(id, (CapabilityStatementComparison) comp); 164 } else if (comp instanceof PlaceHolderComparison) { 165 renderPlaceHolder(id, (PlaceHolderComparison) comp); 166 } 167 } 168 169 private void renderPlaceHolder(String id, PlaceHolderComparison comp) throws IOException { 170 String cnt = ""; 171 if (comp.getE() != null) { 172 StringWriter sw = new StringWriter(); 173 PrintWriter pw = new PrintWriter(sw); 174 comp.getE().printStackTrace(pw); 175 cnt = sw.toString(); 176 } 177 cnt = "<html><body><pre>"+cnt+"</pre></body></html>\r\n"; 178 FileUtilities.stringToFile(cnt, file(comp.getId()+".html")); 179 } 180 181 private void renderCodeSystem(String id, CodeSystemComparison comp) throws IOException, FHIRFormatError, DefinitionException, FHIRException, EOperationOutcome { 182 String template = templates.get("CodeSystem"); 183 Map<String, Base> vars = new HashMap<>(); 184 CodeSystemComparer cs = new CodeSystemComparer(session); 185 vars.put("left", new StringType(comp.getLeft().present())); 186 vars.put("right", new StringType(comp.getRight().present())); 187 vars.put("leftId", new StringType(comp.getLeft().getId())); 188 vars.put("rightId", new StringType(comp.getRight().getId())); 189 vars.put("leftUrl", new StringType(comp.getLeft().getUrl())); 190 vars.put("rightUrl", new StringType(comp.getRight().getUrl())); 191 vars.put("summary", new StringType(comp.summary())); 192 vars.put("errors", new StringType(new XhtmlComposer(true).compose(cs.renderErrors(comp)))); 193 vars.put("metadata", new StringType(new XhtmlComposer(true).compose(cs.renderMetadata(comp, "", "")))); 194 vars.put("concepts", new StringType(new XhtmlComposer(true).compose(cs.renderConcepts(comp, "", "")))); 195 196 String cnt = processTemplate(template, "CodeSystem", vars); 197 FileUtilities.stringToFile(cnt, file(comp.getId()+".html")); 198 new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(folder, comp.getId() + "-union.json")), comp.getUnion()); 199 new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(folder, comp.getId() + "-intersection.json")), comp.getIntersection()); 200 201 String union = new XhtmlComposer(true).compose(cs.renderUnion(comp, "", folder, "http://hl7.org/fhir")); 202 String intersection = new XhtmlComposer(true).compose(cs.renderIntersection(comp, "", folder, "http://hl7.org/fhir")); 203 vars.put("union", new StringType(union)); 204 vars.put("intersection", new StringType(intersection)); 205 206 template = templates.get("CodeSystem-Union"); 207 cnt = processTemplate(template, "CodeSystem-Union", vars); 208 FileUtilities.stringToFile(cnt, file(comp.getId()+"-union.html")); 209 210 template = templates.get("CodeSystem-Intersection"); 211 cnt = processTemplate(template, "CodeSystem-Intersection", vars); 212 FileUtilities.stringToFile(cnt, file(comp.getId()+"-intersection.html")); 213 214 } 215 216 private String file(String name) throws IOException { 217 return Utilities.path(folder, name); 218 } 219 220 private void renderValueSet(String id, ValueSetComparison comp) throws FHIRException, IOException, EOperationOutcome { 221 String template = templates.get("ValueSet"); 222 Map<String, Base> vars = new HashMap<>(); 223 ValueSetComparer cs = new ValueSetComparer(session); 224 vars.put("left", new StringType(comp.getLeft().present())); 225 vars.put("right", new StringType(comp.getRight().present())); 226 vars.put("leftId", new StringType(comp.getLeft().getId())); 227 vars.put("rightId", new StringType(comp.getRight().getId())); 228 vars.put("leftUrl", new StringType(comp.getLeft().getUrl())); 229 vars.put("rightUrl", new StringType(comp.getRight().getUrl())); 230 vars.put("summary", new StringType(comp.summary())); 231 vars.put("errors", new StringType(new XhtmlComposer(true).compose(cs.renderErrors(comp)))); 232 vars.put("metadata", new StringType(new XhtmlComposer(true).compose(cs.renderMetadata(comp, "", "")))); 233 vars.put("compose", new StringType(new XhtmlComposer(true).compose(cs.renderCompose(comp, "", "")))); 234 vars.put("expansion", new StringType(new XhtmlComposer(true).compose(cs.renderExpansion(comp, "", "")))); 235 String cnt = processTemplate(template, "ValueSet", vars); 236 FileUtilities.stringToFile(cnt, file(comp.getId()+".html")); 237 new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(folder, comp.getId() + "-union.json")), comp.getUnion()); 238 new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(folder, comp.getId() + "-intersection.json")), comp.getIntersection()); 239 240 String union = new XhtmlComposer(true).compose(cs.renderUnion(comp, "", folder, "http://hl7.org/fhir")); 241 String intersection = new XhtmlComposer(true).compose(cs.renderIntersection(comp, "", folder, "http://hl7.org/fhir")); 242 vars.put("union", new StringType(union)); 243 vars.put("intersection", new StringType(intersection)); 244 245 template = templates.get("ValueSet-Union"); 246 cnt = processTemplate(template, "ValueSet-Union", vars); 247 FileUtilities.stringToFile(cnt, file(comp.getId()+"-union.html")); 248 249 template = templates.get("ValueSet-Intersection"); 250 cnt = processTemplate(template, "ValueSet-Intersection", vars); 251 FileUtilities.stringToFile(cnt, file(comp.getId()+"-intersection.html")); 252 } 253 254 private void renderProfile(String id, ProfileComparison comp) throws IOException { 255 String template = templates.get("Profile"); 256 Map<String, Base> vars = new HashMap<>(); 257 StructureDefinitionComparer cs = new StructureDefinitionComparer(session, new ProfileUtilities(session.getContextLeft(), null, session.getPkpLeft()), 258 new ProfileUtilities(session.getContextRight(), null, session.getPkpRight())); 259 vars.put("left", new StringType(comp.getLeft().present())); 260 vars.put("right", new StringType(comp.getRight().present())); 261 vars.put("leftId", new StringType(comp.getLeft().getId())); 262 vars.put("rightId", new StringType(comp.getRight().getId())); 263 vars.put("leftUrl", new StringType(comp.getLeft().getUrl())); 264 vars.put("rightUrl", new StringType(comp.getRight().getUrl())); 265 vars.put("errors", new StringType(new XhtmlComposer(true).compose(cs.renderErrors(comp)))); 266 vars.put("metadata", new StringType(new XhtmlComposer(true).compose(cs.renderMetadata(comp, "", "")))); 267 vars.put("structure", new StringType(new XhtmlComposer(true).compose(cs.renderStructure(comp, "", "", "http://hl7.org/fhir")))); 268 String union = new XhtmlComposer(true).compose(cs.renderUnion(comp, "", folder, "http://hl7.org/fhir")); 269 String intersection = new XhtmlComposer(true).compose(cs.renderIntersection(comp, "", folder, "http://hl7.org/fhir")); 270 vars.put("union", new StringType(union)); 271 vars.put("intersection", new StringType(intersection)); 272 273 String cnt = processTemplate(template, "Profile", vars); 274 FileUtilities.stringToFile(cnt, file(comp.getId()+".html")); 275 276 template = templates.get("Profile-Union"); 277 cnt = processTemplate(template, "Profile-Union", vars); 278 FileUtilities.stringToFile(cnt, file(comp.getId()+"-union.html")); 279 280 template = templates.get("Profile-Intersection"); 281 cnt = processTemplate(template, "Profile-Intersection", vars); 282 FileUtilities.stringToFile(cnt, file(comp.getId()+"-intersection.html")); 283 284 new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(folder, comp.getId() + "-union.json")), comp.getUnion()); 285 new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(folder, comp.getId() + "-intersection.json")), comp.getIntersection()); 286 } 287 288 private void renderCapabilityStatement(String id, CapabilityStatementComparison comp) throws IOException { 289 String template = templates.get("CapabilityStatement"); 290 Map<String, Base> vars = new HashMap<>(); 291 CapabilityStatementComparer cs = new CapabilityStatementComparer(session); 292 vars.put("left", new StringType(comp.getLeft().present())); 293 vars.put("right", new StringType(comp.getRight().present())); 294 vars.put("leftId", new StringType(comp.getLeft().getId())); 295 vars.put("rightId", new StringType(comp.getRight().getId())); 296 vars.put("leftUrl", new StringType(comp.getLeft().getUrl())); 297 vars.put("rightUrl", new StringType(comp.getRight().getUrl())); 298 vars.put("errors", new StringType(new XhtmlComposer(true).compose(cs.renderErrors(comp)))); 299 vars.put("metadata", new StringType(new XhtmlComposer(true).compose(cs.renderMetadata(comp, "", "")))); 300 vars.put("statement", new StringType(new XhtmlComposer(true).compose(cs.renderStatements(comp, "", "")))); 301 String cnt = processTemplate(template, "CapabilityStatement", vars); 302 FileUtilities.stringToFile(cnt, file(comp.getId()+".html")); 303 new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(folder, comp.getId() + "-union.json")), comp.getUnion()); 304 new org.hl7.fhir.r5.formats.JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(Utilities.path(folder, comp.getId() + "-intersection.json")), comp.getIntersection()); 305 } 306 307 private String processTemplate(String template, String name, Map<String, Base> vars) { 308 LiquidEngine engine = new LiquidEngine(contextRight, this); 309 LiquidDocument doc = engine.parse(template, name+".template"); 310 return engine.evaluate(doc, Tuple.fromMap(FhirPublication.R5, vars), vars); 311 } 312 313 @Override 314 public List<Base> resolveConstant(FHIRPathEngine engine, Object appContext, String name, FHIRPathConstantEvaluationMode mode) throws PathEngineException { 315 Map<String, Base> vars = (Map<String, Base>) appContext; 316 List<Base> res = new ArrayList<>(); 317 if (mode == FHIRPathConstantEvaluationMode.EXPLICIT) { 318 if (vars.containsKey(name)) { 319 res.add(vars.get(name)); 320 } 321 } 322 return res; 323 } 324 325 @Override 326 public TypeDetails resolveConstantType(FHIRPathEngine engine, Object appContext, String name, FHIRPathConstantEvaluationMode mode) throws PathEngineException { 327 Map<String, Base> vars = (Map<String, Base>) appContext; 328 Base b = mode == FHIRPathConstantEvaluationMode.EXPLICIT ? vars.get(name) : null; 329 return new TypeDetails(CollectionStatus.SINGLETON, b == null ? "Base" : b.fhirType()); 330 } 331 332 @Override 333 public boolean log(String argument, List<Base> focus) { 334 return false; 335 } 336 337 @Override 338 public FunctionDetails resolveFunction(FHIRPathEngine engine, String functionName) { 339 return null; 340 } 341 342 @Override 343 public TypeDetails checkFunction(FHIRPathEngine engine, Object appContext, String functionName, TypeDetails focus, List<TypeDetails> parameters) throws PathEngineException { 344 return null; 345 } 346 347 @Override 348 public List<Base> executeFunction(FHIRPathEngine engine, Object appContext, List<Base> focus, String functionName, List<List<Base>> parameters) { 349 return null; 350 } 351 352 @Override 353 public Base resolveReference(FHIRPathEngine engine, Object appContext, String url, Base refContext) throws FHIRException { 354 return null; 355 } 356 357 @Override 358 public boolean conformsToProfile(FHIRPathEngine engine, Object appContext, Base item, String url) throws FHIRException { 359 return false; 360 } 361 362 @Override 363 public ValueSet resolveValueSet(FHIRPathEngine engine, Object appContext, String url) { 364 return null; 365 } 366 367 @Override 368 public boolean paramIsType(String name, int index) { 369 return false; 370 } 371 372 public void loadTemplates(IWorkerContext context) { 373 getTemplates().put("CapabilityStatement", new String(context.getBinaryForKey("template-comparison-CapabilityStatement.html"))); 374 getTemplates().put("CodeSystem-Intersection", new String(context.getBinaryForKey("template-comparison-CodeSystem-Intersection.html"))); 375 getTemplates().put("CodeSystem-Union", new String(context.getBinaryForKey("template-comparison-CodeSystem-Union.html"))); 376 getTemplates().put("CodeSystem", new String(context.getBinaryForKey("template-comparison-CodeSystem.html"))); 377 getTemplates().put("Index", new String(context.getBinaryForKey("template-comparison-index.html"))); 378 getTemplates().put("Profile-Intersection", new String(context.getBinaryForKey("template-comparison-Profile-Intersection.html"))); 379 getTemplates().put("Profile-Union", new String(context.getBinaryForKey("template-comparison-Profile-Union.html"))); 380 getTemplates().put("Profile", new String(context.getBinaryForKey("template-comparison-Profile.html"))); 381 getTemplates().put("ValueSet-Intersection", new String(context.getBinaryForKey("template-comparison-ValueSet-Intersection.html"))); 382 getTemplates().put("ValueSet-Union", new String(context.getBinaryForKey("template-comparison-ValueSet-Union.html"))); 383 getTemplates().put("ValueSet", new String(context.getBinaryForKey("template-comparison-ValueSet.html"))); 384 385 } 386 387 388 public Base findContainingResource(Object appContext, Base item) { 389 return null; 390 } 391}