001package org.hl7.fhir.validation;
002
003import lombok.Getter;
004import org.hl7.fhir.exceptions.FHIRException;
005import org.hl7.fhir.r5.context.SimpleWorkerContext;
006import org.hl7.fhir.r5.elementmodel.Element;
007import org.hl7.fhir.r5.model.ImplementationGuide;
008import org.hl7.fhir.r5.model.OperationOutcome;
009import org.hl7.fhir.r5.model.StructureDefinition;
010import org.hl7.fhir.r5.renderers.RendererFactory;
011import org.hl7.fhir.r5.renderers.utils.RenderingContext;
012import org.hl7.fhir.r5.utils.EOperationOutcome;
013import org.hl7.fhir.r5.utils.FHIRPathEngine;
014import org.hl7.fhir.utilities.SimpleHTTPClient;
015import org.hl7.fhir.utilities.SimpleHTTPClient.HTTPResult;
016import org.hl7.fhir.utilities.TextFile;
017import org.hl7.fhir.utilities.Utilities;
018import org.hl7.fhir.utilities.validation.ValidationMessage;
019import org.hl7.fhir.utilities.xhtml.XhtmlComposer;
020import org.hl7.fhir.validation.cli.model.ScanOutputItem;
021import org.hl7.fhir.validation.instance.InstanceValidator;
022
023import java.io.*;
024import java.util.*;
025import java.util.stream.Collectors;
026import java.util.zip.ZipEntry;
027import java.util.zip.ZipInputStream;
028
029public class Scanner {
030
031  private static final int BUFFER_SIZE = 4096;
032
033  @Getter private final SimpleWorkerContext context;
034  @Getter private final InstanceValidator validator;
035  @Getter private final IgLoader igLoader;
036  @Getter private final FHIRPathEngine fhirPathEngine;
037
038  public Scanner(SimpleWorkerContext context, InstanceValidator validator, IgLoader igLoader, FHIRPathEngine fhirPathEngine) {
039    this.context = context;
040    this.validator = validator;
041    this.igLoader = igLoader;
042    this.fhirPathEngine = fhirPathEngine;
043  }
044
045  public void validateScan(String output, List<String> sources) throws Exception {
046    if (Utilities.noString(output))
047      throw new Exception("Output parameter required when scanning");
048    if (!(new File(output).isDirectory()))
049      throw new Exception("Output '" + output + "' must be a directory when scanning");
050    System.out.println("  .. scan " + sources + " against loaded IGs");
051    Set<String> urls = new HashSet<>();
052    for (ImplementationGuide ig : getContext().allImplementationGuides()) {
053      if (ig.getUrl().contains("/ImplementationGuide") && !ig.getUrl().equals("http://hl7.org/fhir/ImplementationGuide/fhir"))
054        urls.add(ig.getUrl());
055    }
056    List<ScanOutputItem> res = validateScan(sources, urls);
057    genScanOutput(output, res);
058    System.out.println("Done. output in " + Utilities.path(output, "scan.html"));
059  }
060
061  protected List<ScanOutputItem> validateScan(List<String> sources, Set<String> guides) throws FHIRException, IOException, EOperationOutcome {
062    List<String> refs = new ArrayList<>();
063    ValidatorUtils.parseSources(sources, refs, getContext());
064
065    List<ScanOutputItem> res = new ArrayList();
066
067    for (String ref : refs) {
068      Content cnt = getIgLoader().loadContent(ref, "validate", false);
069      List<ValidationMessage> messages = new ArrayList<>();
070      Element e = null;
071      try {
072        System.out.println("Validate " + ref);
073        messages.clear();
074        e = getValidator().validate(null, messages, new ByteArrayInputStream(cnt.focus), cnt.cntType);
075        res.add(new ScanOutputItem(ref, null, null, ValidatorUtils.messagesToOutcome(messages, getContext(), getFhirPathEngine())));
076      } catch (Exception ex) {
077        res.add(new ScanOutputItem(ref, null, null, exceptionToOutcome(ex)));
078      }
079      if (e != null) {
080        String rt = e.fhirType();
081        for (String u : guides) {
082          ImplementationGuide ig = getContext().fetchResource(ImplementationGuide.class, u);
083          System.out.println("Check Guide " + ig.getUrl());
084          String canonical = ig.getUrl().contains("/Impl") ? ig.getUrl().substring(0, ig.getUrl().indexOf("/Impl")) : ig.getUrl();
085          String url = getGlobal(ig, rt);
086          if (url != null) {
087            try {
088              System.out.println("Validate " + ref + " against " + ig.getUrl());
089              messages.clear();
090              getValidator().validate(null, messages, new ByteArrayInputStream(cnt.focus), cnt.cntType, url);
091              res.add(new ScanOutputItem(ref, ig, null, ValidatorUtils.messagesToOutcome(messages, getContext(), getFhirPathEngine())));
092            } catch (Exception ex) {
093              res.add(new ScanOutputItem(ref, ig, null, exceptionToOutcome(ex)));
094            }
095          }
096          Set<String> done = new HashSet<>();
097          for (StructureDefinition sd : getContext().allStructures()) {
098            if (!done.contains(sd.getUrl())) {
099              done.add(sd.getUrl());
100              if (sd.getUrl().startsWith(canonical) && rt.equals(sd.getType())) {
101                try {
102                  System.out.println("Validate " + ref + " against " + sd.getUrl());
103                  messages.clear();
104                  validator.validate(null, messages, new ByteArrayInputStream(cnt.focus), cnt.cntType, Collections.singletonList(sd));
105                  res.add(new ScanOutputItem(ref, ig, sd, ValidatorUtils.messagesToOutcome(messages, getContext(), getFhirPathEngine())));
106                } catch (Exception ex) {
107                  res.add(new ScanOutputItem(ref, ig, sd, exceptionToOutcome(ex)));
108                }
109              }
110            }
111          }
112        }
113      }
114    }
115    return res;
116  }
117
118  protected void genScanOutput(String folder, List<ScanOutputItem> items) throws IOException, FHIRException, EOperationOutcome {
119    String f = Utilities.path(folder, "comparison.zip");
120    download("http://fhir.org/archive/comparison.zip", f);
121    unzip(f, folder);
122
123    for (int i = 0; i < items.size(); i++) {
124      items.get(i).setId("c" + i);
125      genScanOutputItem(items.get(i), Utilities.path(folder, items.get(i).getId() + ".html"));
126    }
127
128    StringBuilder b = new StringBuilder();
129    b.append("<html>");
130    b.append("<head>");
131    b.append("<title>Implementation Guide Scan</title>");
132    b.append("<link rel=\"stylesheet\" href=\"fhir.css\"/>\r\n");
133    b.append("<style>\r\n");
134    b.append("th \r\n");
135    b.append("{\r\n");
136    b.append("  vertical-align: bottom;\r\n");
137    b.append("  text-align: center;\r\n");
138    b.append("}\r\n");
139    b.append("\r\n");
140    b.append("th span\r\n");
141    b.append("{\r\n");
142    b.append("  -ms-writing-mode: tb-rl;\r\n");
143    b.append("  -webkit-writing-mode: vertical-rl;\r\n");
144    b.append("  writing-mode: vertical-rl;\r\n");
145    b.append("  transform: rotate(180deg);\r\n");
146    b.append("  white-space: nowrap;\r\n");
147    b.append("}\r\n");
148    b.append("</style>\r\n");
149    b.append("</head>");
150    b.append("<body>");
151    b.append("<h2>Implementation Guide Scan</h2>");
152
153    // organise
154    Set<String> refs = new HashSet<>();
155    Set<String> igs = new HashSet<>();
156    Map<String, Set<String>> profiles = new HashMap<>();
157    for (ScanOutputItem item : items) {
158      refs.add(item.getRef());
159      if (item.getIg() != null) {
160        igs.add(item.getIg().getUrl());
161        if (!profiles.containsKey(item.getIg().getUrl())) {
162          profiles.put(item.getIg().getUrl(), new HashSet<>());
163        }
164        if (item.getProfile() != null)
165          profiles.get(item.getIg().getUrl()).add(item.getProfile().getUrl());
166      }
167    }
168
169    b.append("<h2>By reference</h2>\r\n");
170    b.append("<table class=\"grid\">");
171    b.append("<tr><th></th><th></th>");
172    for (String s : sort(igs)) {
173      ImplementationGuide ig = getContext().fetchResource(ImplementationGuide.class, s);
174      b.append("<th colspan=\"" + Integer.toString(profiles.get(s).size() + 1) + "\"><b title=\"" + s + "\">" + ig.present() + "</b></th>");
175    }
176    b.append("</tr>\r\n");
177    b.append("<tr><th><b>Source</b></th><th><span>Core Spec</span></th>");
178    for (String s : sort(igs)) {
179      ImplementationGuide ig = getContext().fetchResource(ImplementationGuide.class, s);
180      b.append("<th><span>Global</span></th>");
181      for (String sp : sort(profiles.get(s))) {
182        StructureDefinition sd = getContext().fetchResource(StructureDefinition.class, sp);
183        b.append("<th><b title=\"" + sp + "\"><span>" + sd.present() + "</span></b></th>");
184      }
185    }
186    b.append("</tr>\r\n");
187
188    for (String s : sort(refs)) {
189      b.append("<tr>");
190      b.append("<td>" + s + "</td>");
191      b.append(genOutcome(items, s, null, null));
192      for (String si : sort(igs)) {
193        ImplementationGuide ig = getContext().fetchResource(ImplementationGuide.class, si);
194        b.append(genOutcome(items, s, si, null));
195        for (String sp : sort(profiles.get(ig.getUrl()))) {
196          b.append(genOutcome(items, s, si, sp));
197        }
198      }
199      b.append("</tr>\r\n");
200    }
201    b.append("</table>\r\n");
202
203    b.append("<h2>By IG</h2>\r\n");
204    b.append("<table class=\"grid\">");
205    b.append("<tr><th></th><th></th>");
206    for (String s : sort(refs)) {
207      b.append("<th><span>" + s + "</span></th>");
208    }
209    b.append("</tr>\r\n");
210    b.append("<tr><td></td><td>Core Spec</td>");
211    for (String s : sort(refs)) {
212      b.append(genOutcome(items, s, null, null));
213    }
214    b.append("</tr>\r\n");
215    for (String si : sort(igs)) {
216      b.append("<tr>");
217      ImplementationGuide ig = getContext().fetchResource(ImplementationGuide.class, si);
218      b.append("<td><b title=\"" + si + "\">" + ig.present() + "</b></td>");
219      b.append("<td>Global</td>");
220      for (String s : sort(refs)) {
221        b.append(genOutcome(items, s, si, null));
222      }
223      b.append("</tr>\r\n");
224
225      for (String sp : sort(profiles.get(ig.getUrl()))) {
226        b.append("<tr>");
227        StructureDefinition sd = getContext().fetchResource(StructureDefinition.class, sp);
228        b.append("<td></td><td><b title=\"" + sp + "\">" + sd.present() + "</b></td>");
229        for (String s : sort(refs)) {
230          b.append(genOutcome(items, s, si, sp));
231        }
232        b.append("</tr>\r\n");
233      }
234    }
235    b.append("</table>\r\n");
236
237    b.append("</body>");
238    b.append("</html>");
239    TextFile.stringToFile(b.toString(), Utilities.path(folder, "scan.html"));
240  }
241
242  protected void genScanOutputItem(ScanOutputItem item, String filename) throws IOException, FHIRException, EOperationOutcome {
243    RenderingContext rc = new RenderingContext(getContext(), null, null, "http://hl7.org/fhir", "", null, RenderingContext.ResourceRendererMode.END_USER);
244    rc.setNoSlowLookup(true);
245    RendererFactory.factory(item.getOutcome(), rc).render(item.getOutcome());
246    String s = new XhtmlComposer(XhtmlComposer.HTML).compose(item.getOutcome().getText().getDiv());
247
248    String title = item.getTitle();
249
250    StringBuilder b = new StringBuilder();
251    b.append("<html>");
252    b.append("<head>");
253    b.append("<title>" + title + "</title>");
254    b.append("<link rel=\"stylesheet\" href=\"fhir.css\"/>\r\n");
255    b.append("</head>");
256    b.append("<body>");
257    b.append("<h2>" + title + "</h2>");
258    b.append(s);
259    b.append("</body>");
260    b.append("</html>");
261    TextFile.stringToFile(b.toString(), filename);
262  }
263
264  protected String genOutcome(List<ScanOutputItem> items, String src, String ig, String profile) {
265    ScanOutputItem item = null;
266    for (ScanOutputItem t : items) {
267      boolean match = true;
268      if (!t.getRef().equals(src))
269        match = false;
270      if (!((ig == null && t.getIg() == null) || (ig != null && t.getIg() != null && ig.equals(t.getIg().getUrl()))))
271        match = false;
272      if (!((profile == null && t.getProfile() == null) || (profile != null && t.getProfile() != null && profile.equals(t.getProfile().getUrl()))))
273        match = false;
274      if (match) {
275        item = t;
276        break;
277      }
278    }
279
280    if (item == null)
281      return "<td></td>";
282    boolean ok = true;
283    for (OperationOutcome.OperationOutcomeIssueComponent iss : item.getOutcome().getIssue()) {
284      if (iss.getSeverity() == OperationOutcome.IssueSeverity.ERROR || iss.getSeverity() == OperationOutcome.IssueSeverity.FATAL) {
285        ok = false;
286      }
287    }
288    if (ok)
289      return "<td style=\"background-color: #e6ffe6\"><a href=\"" + item.getId() + ".html\">\u2714</a></td>";
290    else
291      return "<td style=\"background-color: #ffe6e6\"><a href=\"" + item.getId() + ".html\">\u2716</a></td>";
292  }
293
294  protected OperationOutcome exceptionToOutcome(Exception ex) throws IOException, FHIRException, EOperationOutcome {
295    OperationOutcome op = new OperationOutcome();
296    op.addIssue().setCode(OperationOutcome.IssueType.EXCEPTION).setSeverity(OperationOutcome.IssueSeverity.FATAL).getDetails().setText(ex.getMessage());
297    RenderingContext rc = new RenderingContext(getContext(), null, null, "http://hl7.org/fhir", "", null, RenderingContext.ResourceRendererMode.END_USER);
298    RendererFactory.factory(op, rc).render(op);
299    return op;
300  }
301
302  protected void download(String address, String filename) throws IOException {
303    SimpleHTTPClient http = new SimpleHTTPClient();
304    HTTPResult res = http.get(address);
305    res.checkThrowException();
306    TextFile.bytesToFile(res.getContent(), filename);
307  }
308
309  protected void transfer(InputStream in, OutputStream out, int buffer) throws IOException {
310    byte[] read = new byte[buffer]; // Your buffer size.
311    while (0 < (buffer = in.read(read)))
312      out.write(read, 0, buffer);
313  }
314
315  protected List<String> sort(Set<String> keys) {
316    return keys.stream().sorted().collect(Collectors.toList());
317  }
318
319  protected void unzip(String zipFilePath, String destDirectory) throws IOException {
320    File destDir = new File(destDirectory);
321    if (!destDir.exists()) {
322      destDir.mkdir();
323    }
324    ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
325    ZipEntry entry = zipIn.getNextEntry();
326    // iterates over entries in the zip file
327    while (entry != null) {
328      String filePath = destDirectory + File.separator + entry.getName();
329      if (!entry.isDirectory()) {
330        // if the entry is a file, extracts it
331        extractFile(zipIn, filePath);
332      } else {
333        // if the entry is a directory, make the directory
334        File dir = new File(filePath);
335        dir.mkdir();
336      }
337      zipIn.closeEntry();
338      entry = zipIn.getNextEntry();
339    }
340    zipIn.close();
341  }
342
343  protected void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
344    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
345    byte[] bytesIn = new byte[BUFFER_SIZE];
346    int read;
347    while ((read = zipIn.read(bytesIn)) != -1) {
348      bos.write(bytesIn, 0, read);
349    }
350    bos.close();
351  }
352
353  protected String getGlobal(ImplementationGuide ig, String rt) {
354    for (ImplementationGuide.ImplementationGuideGlobalComponent igg : ig.getGlobal()) {
355      if (rt.equals(igg.getType()))
356        return igg.getProfile();
357    }
358    return null;
359  }
360}