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}