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