001package org.hl7.fhir.validation.cli.services; 002 003import java.io.IOException; 004import java.net.MalformedURLException; 005import java.net.URISyntaxException; 006import java.util.ArrayList; 007import java.util.HashMap; 008import java.util.List; 009import java.util.Locale; 010import java.util.Map; 011 012import org.hl7.fhir.convertors.txClient.TerminologyClientFactory; 013import org.hl7.fhir.exceptions.FHIRException; 014import org.hl7.fhir.r5.context.IWorkerContext; 015import org.hl7.fhir.r5.context.IWorkerContextManager; 016import org.hl7.fhir.r5.elementmodel.Element; 017import org.hl7.fhir.r5.model.CanonicalResource; 018import org.hl7.fhir.r5.model.ElementDefinition; 019import org.hl7.fhir.r5.model.Resource; 020import org.hl7.fhir.r5.model.StructureDefinition; 021import org.hl7.fhir.r5.model.ValueSet; 022import org.hl7.fhir.r5.terminologies.TerminologyClient; 023import org.hl7.fhir.r5.utils.validation.IResourceValidator; 024import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; 025import org.hl7.fhir.r5.utils.validation.IValidatorResourceFetcher; 026import org.hl7.fhir.r5.utils.validation.constants.BindingKind; 027import org.hl7.fhir.r5.utils.validation.constants.CodedContentValidationPolicy; 028import org.hl7.fhir.r5.utils.validation.constants.ContainedReferenceValidationPolicy; 029import org.hl7.fhir.r5.utils.validation.constants.ReferenceValidationPolicy; 030import org.hl7.fhir.utilities.Utilities; 031import org.hl7.fhir.utilities.VersionUtilities; 032import org.hl7.fhir.utilities.VersionUtilities.VersionURLInfo; 033import org.hl7.fhir.utilities.json.model.JsonObject; 034import org.hl7.fhir.utilities.json.parser.JsonParser; 035import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; 036import org.hl7.fhir.utilities.npm.NpmPackage; 037 038 039public class StandAloneValidatorFetcher implements IValidatorResourceFetcher, IValidationPolicyAdvisor, IWorkerContextManager.ICanonicalResourceLocator { 040 041 List<String> mappingsUris = new ArrayList<>(); 042 private FilesystemPackageCacheManager pcm; 043 private IWorkerContext context; 044 private IPackageInstaller installer; 045 private Map<String, Boolean> urlList = new HashMap<>(); 046 private Map<String, String> pidList = new HashMap<>(); 047 private Map<String, NpmPackage> pidMap = new HashMap<>(); 048 049 public StandAloneValidatorFetcher(FilesystemPackageCacheManager pcm, IWorkerContext context, IPackageInstaller installer) { 050 super(); 051 this.pcm = pcm; 052 this.context = context; 053 this.installer = installer; 054 } 055 056 @Override 057 public Element fetch(IResourceValidator validator, Object appContext, String url) throws FHIRException { 058 throw new FHIRException("The URL '" + url + "' is not known to the FHIR validator, and has not been provided as part of the setup / parameters"); 059 } 060 061 @Override 062 public ReferenceValidationPolicy policyForReference(IResourceValidator validator, 063 Object appContext, 064 String path, 065 String url) { 066 return ReferenceValidationPolicy.CHECK_TYPE_IF_EXISTS; 067 } 068 069 @Override 070 public ContainedReferenceValidationPolicy policyForContained(IResourceValidator validator, 071 Object appContext, 072 String containerType, 073 String containerId, 074 Element.SpecialElement containingResourceType, 075 String path, 076 String url) { 077 return ContainedReferenceValidationPolicy.CHECK_VALID; 078 } 079 080 @Override 081 public boolean resolveURL(IResourceValidator validator, Object appContext, String path, String url, String type) throws IOException, FHIRException { 082 if (!Utilities.isAbsoluteUrl(url)) { 083 return false; 084 } 085 086 if (url.contains("|")) { 087 url = url.substring(0, url.lastIndexOf("|")); 088 } 089 090 if (type != null && type.equals("uri") && isMappingUri(url)) { 091 return true; 092 } 093 094 // if we've got to here, it's a reference to a FHIR URL. We're going to try to resolve it on the fly 095 String pid = null; 096 String ver = null; 097 String base = findBaseUrl(url); 098 if (base == null) { 099 return !url.startsWith("http://hl7.org/fhir") && !type.equals("canonical"); 100 } 101 102 // the next operations are expensive. we're going to cache them 103 if (urlList.containsKey(url)) { 104 return urlList.get(url); 105 } 106 if (base.equals("http://terminology.hl7.org")) { 107 pid = "hl7.terminology"; 108 } else if (url.startsWith("http://hl7.org/fhir")) { 109 pid = pcm.getPackageId(base); 110 } else { 111 if (pidList.containsKey(base)) { 112 pid = pidList.get(base); 113 } else { 114 pid = pcm.findCanonicalInLocalCache(base); 115 pidList.put(base, pid); 116 } 117 } 118 ver = url.contains("|") ? url.substring(url.indexOf("|") + 1) : null; 119 if (pid == null && Utilities.startsWithInList(url, "http://hl7.org/fhir", "http://terminology.hl7.org")) { 120 urlList.put(url, false); 121 return false; 122 } 123 124 if (url.startsWith("http://hl7.org/fhir")) { 125 // first possibility: it's a reference to a version specific URL http://hl7.org/fhir/X.X/... 126 VersionURLInfo vu = VersionUtilities.parseVersionUrl(url); 127 if (vu != null) { 128 NpmPackage pi = pcm.loadPackage(VersionUtilities.packageForVersion(vu.getVersion()), VersionUtilities.getCurrentVersion(vu.getVersion())); 129 boolean res = pi.hasCanonical(vu.getUrl()); 130 urlList.put(url, res); 131 return res; 132 } 133 } 134 135 // ok maybe it's a reference to a package we know 136 if (pid != null) { 137 if ("sharedhealth.fhir.ca.common".equals(pid)) { // special case - optimise this 138 return false; 139 } 140 NpmPackage pi = null; 141 if (pidMap.containsKey(pid+"|"+ver)) { 142 pi = pidMap.get(pid+"|"+ver); 143 } else if (installer.packageExists(pid, ver)) { 144 try { 145 installer.loadPackage(pid, ver); 146 pi = pcm.loadPackage(pid); 147 pidMap.put(pid+"|"+ver, pi); 148 } catch (Exception e) { 149 pidMap.put(pid+"|"+ver, null); 150 } 151 } else { 152 pidMap.put(pid+"|"+ver, null); 153 } 154 if (pi != null) { 155 context.loadFromPackage(pi, null); 156 return pi.hasCanonical(url) || context.fetchResource(Resource.class, url) != null; 157 } 158 } 159 160 // we don't bother with urls outside fhir space in the standalone validator - we assume they are valid 161 return !url.startsWith("http://hl7.org/fhir") && !type.equals("canonical"); 162 } 163 164 private boolean isMappingUri(String url) { 165 if (mappingsUris.isEmpty()) { 166 JsonObject json; 167 try { 168 json = JsonParser.parseObjectFromUrl("http://hl7.org/fhir/mappingspaces.json"); 169 for (JsonObject ms : json.getJsonObjects("spaces")) { 170 mappingsUris.add(ms.asString("url")); 171 } 172 } catch (IOException e) { 173 // frozen R4 list 174 mappingsUris.add("http://hl7.org/fhir/fivews"); 175 mappingsUris.add("http://hl7.org/fhir/workflow"); 176 mappingsUris.add("http://hl7.org/fhir/interface"); 177 mappingsUris.add("http://hl7.org/v2"); 178 mappingsUris.add("http://loinc.org"); 179 mappingsUris.add("http://snomed.org/attributebinding"); 180 mappingsUris.add("http://snomed.info/conceptdomain"); 181 mappingsUris.add("http://hl7.org/v3/cda"); 182 mappingsUris.add("http://hl7.org/v3"); 183 mappingsUris.add("http://nema.org/dicom"); 184 mappingsUris.add("http://w3.org/vcard"); 185 mappingsUris.add("http://ihe.net/xds"); 186 mappingsUris.add("http://www.w3.org/ns/prov"); 187 mappingsUris.add("http://ietf.org/rfc/2445"); 188 mappingsUris.add("http://www.omg.org/spec/ServD/1.0/"); 189 mappingsUris.add("http://metadata-standards.org/11179/"); 190 mappingsUris.add("http://ihe.net/data-element-exchange"); 191 mappingsUris.add("http://openehr.org"); 192 mappingsUris.add("http://siframework.org/ihe-sdc-profile"); 193 mappingsUris.add("http://siframework.org/cqf"); 194 mappingsUris.add("http://www.cdisc.org/define-xml"); 195 mappingsUris.add("http://www.cda-adc.ca/en/services/cdanet/"); 196 mappingsUris.add("http://www.pharmacists.ca/"); 197 mappingsUris.add("http://www.healthit.gov/quality-data-model"); 198 mappingsUris.add("http://hl7.org/orim"); 199 mappingsUris.add("http://hl7.org/fhir/w5"); 200 mappingsUris.add("http://hl7.org/fhir/logical"); 201 mappingsUris.add("http://hl7.org/fhir/auditevent"); 202 mappingsUris.add("http://hl7.org/fhir/provenance"); 203 mappingsUris.add("http://hl7.org/qidam"); 204 mappingsUris.add("http://cap.org/ecc"); 205 mappingsUris.add("http://fda.gov/UDI"); 206 mappingsUris.add("http://hl7.org/fhir/object-implementation"); 207 mappingsUris.add("http://github.com/MDMI/ReferentIndexContent"); 208 mappingsUris.add("http://ncpdp.org/SCRIPT10_6"); 209 mappingsUris.add("http://clinicaltrials.gov"); 210 mappingsUris.add("http://hl7.org/fhir/rr"); 211 mappingsUris.add("http://www.hl7.org/v3/PORX_RM020070UV"); 212 mappingsUris.add("https://bridgmodel.nci.nih.gov"); 213 mappingsUris.add("http://hl7.org/fhir/composition"); 214 mappingsUris.add("http://hl7.org/fhir/documentreference"); 215 mappingsUris.add("https://en.wikipedia.org/wiki/Identification_of_medicinal_products"); 216 mappingsUris.add("urn:iso:std:iso:11073:10201"); 217 mappingsUris.add("urn:iso:std:iso:11073:10207"); 218 } 219 } 220 return mappingsUris.contains(url); 221 } 222 223 private String findBaseUrl(String url) { 224 String[] p = url.split("\\/"); 225 for (int i = 1; i < p.length; i++) { 226 if (Utilities.existsInList(p[i], context.getResourceNames())) { 227 StringBuilder b = new StringBuilder(p[0]); 228 for (int j = 1; j < i; j++) { 229 b.append("/"); 230 b.append(p[j]); 231 } 232 return b.toString(); 233 } 234 } 235 return null; 236 } 237 238 @Override 239 public byte[] fetchRaw(IResourceValidator validator, String url) throws MalformedURLException, IOException { 240 throw new FHIRException("The URL '" + url + "' is not known to the FHIR validator, and has not been provided as part of the setup / parameters"); 241 } 242 243 @Override 244 public IValidatorResourceFetcher setLocale(Locale locale) { 245 // nothing 246 247 return null; 248 } 249 250 @Override 251 public CanonicalResource fetchCanonicalResource(IResourceValidator validator, String url) throws URISyntaxException { 252 if (url.contains("|")) { 253 url = url.substring(0, url.indexOf("|")); 254 } 255 String[] p = url.split("\\/"); 256 257 String root = getRoot(p, url); 258 if (root != null) { 259 TerminologyClient c; 260 c = TerminologyClientFactory.makeClient(root, "fhir/validator", context.getVersion()); 261 return c.read(p[p.length - 2], p[p.length - 1]); 262 } else { 263 throw new FHIRException("The URL '" + url + "' is not known to the FHIR validator, and has not been provided as part of the setup / parameters"); 264 } 265 } 266 267 private String getRoot(String[] p, String url) { 268 if (p.length > 3 && Utilities.isValidId(p[p.length - 1]) && context.getResourceNames().contains(p[p.length - 2])) { 269 url = url.substring(0, url.lastIndexOf("/")); 270 return url.substring(0, url.lastIndexOf("/")); 271 } else { 272 return null; 273 } 274 } 275 276 @Override 277 public boolean fetchesCanonicalResource(IResourceValidator validator, String url) { 278 return true; 279 } 280 281 @Override 282 public void findResource(Object validator, String url) { 283 try { 284 resolveURL((IResourceValidator) validator, null, null, url, null); 285 } catch (Exception e) { 286 } 287 } 288 289 @Override 290 public CodedContentValidationPolicy policyForCodedContent(IResourceValidator validator, Object appContext, String stackPath, ElementDefinition definition, 291 StructureDefinition structure, BindingKind kind, ValueSet valueSet, List<String> systems) { 292 return CodedContentValidationPolicy.VALUESET; 293 } 294 295}