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}