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