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