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}