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