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}