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}