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}