From 5299bbe16b722a17616367729dca804d02a106c5 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 3 Aug 2020 17:40:21 +1000 Subject: [PATCH] * better validation of external references. Note: this is a potentially significant change: things that were called ok before may not be now, and things that were not ok before may become so, depending on the interplay between this and auto-load, further work may be needed here * Support better validation of version specific profiles in meta.profile. This may also find new errors that were not previously being found * Support auto-determination of the version of FHIR to use when using the java validator * auto-load packages from the package server when references to profiles etc are encountered * look for references inside other parameters in Parameters resource --- RELEASE_NOTES.md | 18 + .../fhir/r5/context/SimpleWorkerContext.java | 5 + .../hl7/fhir/r5/renderers/utils/Resolver.java | 95 +++-- .../hl7/fhir/utilities/VersionUtilities.java | 31 ++ .../hl7/fhir/utilities/cache/NpmPackage.java | 20 ++ .../fhir/utilities/i18n/I18nConstants.java | 5 + .../src/main/resources/Messages.properties | 4 + .../hl7/fhir/validation/ValidationEngine.java | 334 ++++++++++++++---- .../org/hl7/fhir/validation/Validator.java | 27 ++ .../fhir/validation/cli/model/CliContext.java | 2 +- .../services/StandAloneValidatorFetcher.java | 109 ++++++ .../cli/services/ValidationService.java | 11 + .../instance/InstanceValidator.java | 97 ++++- 13 files changed, 653 insertions(+), 105 deletions(-) create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e69de29bb..1fea49ef9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -0,0 +1,18 @@ +Validator Fixes: +* Support auto-determination of the version of FHIR to use when using the java validator +* auto-load packages from the package server when references to profiles etc are encountered +* Support better validation of version specific profiles in meta.profile +* look for references inside other parameters in Parameters resource + +Other Code changes: +* Rendering: add rendering for Parameters resources +* Rendering: refactor of resource resolution code to support Parameters +* General clean up of rendering consistency & implement additional details when rendering (including patient summary) +* Rendering: major overhaul of DiagnosticReport rendering +* Fix NPE bug in value set comparison + + +TODO before commit: +* check version of contained resources +* review validation of CanonicalResource.url + diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/SimpleWorkerContext.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/SimpleWorkerContext.java index a04fb8a1f..3a87b4ff1 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/SimpleWorkerContext.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/context/SimpleWorkerContext.java @@ -267,6 +267,11 @@ public class SimpleWorkerContext extends BaseWorkerContext implements IWorkerCon return res; } + public static SimpleWorkerContext fromNothing() throws FileNotFoundException, FHIRException, IOException { + SimpleWorkerContext res = new SimpleWorkerContext(); + return res; + } + private void loadDefinitionItem(String name, InputStream stream, IContextResourceLoader loader, ILoadFilter filter, PackageVersion pi) throws IOException, FHIRException { if (name.endsWith(".xml")) loadFromFile(stream, name, loader, filter); diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/Resolver.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/Resolver.java index c39b654a7..94c413b31 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/Resolver.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/Resolver.java @@ -3,46 +3,58 @@ package org.hl7.fhir.r5.renderers.utils; import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r5.model.DomainResource; +import org.hl7.fhir.r5.model.Parameters; +import org.hl7.fhir.r5.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.renderers.utils.BaseWrappers.ResourceWrapper; +import org.hl7.fhir.r5.renderers.utils.Resolver.ResourceContextType; import org.w3c.dom.Element; public class Resolver { + public enum ResourceContextType { + PARAMETERS, BUNDLE + } + public interface IReferenceResolver { ResourceWithReference resolve(RenderingContext context, String url); } public static class ResourceContext { - Bundle bundleResource; - org.hl7.fhir.r5.elementmodel.Element bundleElement; + private ResourceContextType type; + private Resource containerResource; + private org.hl7.fhir.r5.elementmodel.Element containerElement; DomainResource resourceResource; org.hl7.fhir.r5.elementmodel.Element resourceElement; - public ResourceContext(Bundle bundle, DomainResource dr) { + public ResourceContext(ResourceContextType type, Resource bundle, DomainResource dr) { super(); - this.bundleResource = bundle; + this.type = type; + this.containerResource = bundle; this.resourceResource = dr; } - public ResourceContext(org.hl7.fhir.r5.elementmodel.Element bundle, org.hl7.fhir.r5.elementmodel.Element dr) { - this.bundleElement = bundle; + public ResourceContext(ResourceContextType type, org.hl7.fhir.r5.elementmodel.Element bundle, org.hl7.fhir.r5.elementmodel.Element dr) { + super(); + this.type = type; + this.containerElement = bundle; this.resourceElement = dr; } - public ResourceContext(Object bundle, Element doc) { - // TODO Auto-generated constructor stub - } +// public ResourceContext(Object bundle, Element doc) { +// // TODO Auto-generated constructor stub +// } - public Bundle getBundleResource() { - return bundleResource; - } +// public Bundle getBundleResource() { +// return containerResource; +// } - public org.hl7.fhir.r5.elementmodel.Element getBundleElement() { - return bundleElement; - } + // public org.hl7.fhir.r5.elementmodel.Element getBundleElement() { +// return containerElement; +// } +// public DomainResource getResourceResource() { return resourceResource; } @@ -64,13 +76,27 @@ public class Resolver { } return null; } - if (bundleResource != null) { - for (BundleEntryComponent be : bundleResource.getEntry()) { - if (be.getFullUrl().equals(value)) - return be; - if (value.equals(be.getResource().fhirType()+"/"+be.getResource().getId())) - return be; - } + if (type == ResourceContextType.BUNDLE) { + if (containerResource != null) { + for (BundleEntryComponent be : ((Bundle) containerResource).getEntry()) { + if (be.getFullUrl().equals(value)) + return be; + if (value.equals(be.getResource().fhirType()+"/"+be.getResource().getId())) + return be; + } + } + } + if (type == ResourceContextType.PARAMETERS) { + if (containerResource != null) { + for (ParametersParameterComponent p : ((Parameters) containerResource).getParameter()) { + if (p.getResource() != null && value.equals(p.getResource().fhirType()+"/"+p.getResource().getId())) { + BundleEntryComponent be = new BundleEntryComponent(); + be.setResource(p.getResource()); + return be; + + } + } + } } return null; } @@ -85,13 +111,24 @@ public class Resolver { } return null; } - if (bundleElement != null) { - for (org.hl7.fhir.r5.elementmodel.Element be : bundleElement.getChildren("entry")) { - org.hl7.fhir.r5.elementmodel.Element res = be.getNamedChild("resource"); - if (value.equals(be.getChildValue("fullUrl"))) - return be; - if (value.equals(res.fhirType()+"/"+res.getChildValue("id"))) - return be; + if (type == ResourceContextType.BUNDLE) { + if (containerElement != null) { + for (org.hl7.fhir.r5.elementmodel.Element be : containerElement.getChildren("entry")) { + org.hl7.fhir.r5.elementmodel.Element res = be.getNamedChild("resource"); + if (value.equals(be.getChildValue("fullUrl"))) + return be; + if (value.equals(res.fhirType()+"/"+res.getChildValue("id"))) + return be; + } + } + } + if (type == ResourceContextType.PARAMETERS) { + if (containerElement != null) { + for (org.hl7.fhir.r5.elementmodel.Element p : containerElement.getChildren("parameter")) { + org.hl7.fhir.r5.elementmodel.Element res = p.getNamedChild("resource"); + if (value.equals(res.fhirType()+"/"+res.getChildValue("id"))) + return p; + } } } return null; diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/VersionUtilities.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/VersionUtilities.java index 8201d97a9..544f83079 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/VersionUtilities.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/VersionUtilities.java @@ -1,6 +1,7 @@ package org.hl7.fhir.utilities; import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.utilities.cache.NpmPackage; /* Copyright (c) 2011+, HL7, Inc. @@ -35,6 +36,22 @@ import org.hl7.fhir.exceptions.FHIRException; public class VersionUtilities { + public static class VersionURLInfo { + private String version; + private String url; + public VersionURLInfo(String version, String url) { + super(); + this.version = version; + this.url = url; + } + public String getVersion() { + return version; + } + public String getUrl() { + return url; + } + } + public static final String CURRENT_VERSION = "4.4"; public static final String CURRENT_FULL_VERSION = "4.4.0"; @@ -225,4 +242,18 @@ public class VersionUtilities { throw new FHIRException("Unknown version "+version); } + public static VersionURLInfo parseVersionUrl(String url) { + if (url.length() < 24) { + return null; + } + String v = url.substring(20, 24); + if (v.endsWith("/")) { + v = v.substring(0, v.length()-1); + if (Utilities.existsInList(v, "1.0", "1.4", "3.0", "4.0", "5.0", CURRENT_VERSION)) { + return new VersionURLInfo(v, "http://hl7.org/fhir/"+url.substring(24)); + } + } + return null; + } + } \ No newline at end of file diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/NpmPackage.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/NpmPackage.java index 166961e17..334ab7d4d 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/NpmPackage.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/cache/NpmPackage.java @@ -1057,6 +1057,26 @@ public class NpmPackage { public boolean isCore() { return "fhir.core".equals(JSONUtil.str(npm, "type")); } + + public boolean hasCanonical(String url) { + if (url == null) { + return false; + } + String u = url.contains("|") ? url.substring(0, url.indexOf("|")) : url; + String v = url.contains("|") ? url.substring(url.indexOf("|")+1) : null; + NpmPackageFolder folder = folders.get("package"); + if (folder != null) { + for (JsonElement e : folder.index.getAsJsonArray("files")) { + JsonObject o = (JsonObject) e; + if (u.equals(JSONUtil.str(o, "url"))) { + if (v == null || v.equals(JSONUtil.str(o, "version"))) { + return true; + } + } + } + } + return false; + } } \ No newline at end of file diff --git a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java index 34b8cc05c..c70ce1748 100644 --- a/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java +++ b/org.hl7.fhir.utilities/src/main/java/org/hl7/fhir/utilities/i18n/I18nConstants.java @@ -25,6 +25,7 @@ public class I18nConstants { public static final String BUNDLE_BUNDLE_ENTRY_NOFIRSTRESOURCE = "Bundle_BUNDLE_Entry_NoFirstResource"; public static final String BUNDLE_BUNDLE_ENTRY_NOFULLURL = "Bundle_BUNDLE_Entry_NoFullUrl"; public static final String BUNDLE_BUNDLE_ENTRY_NOPROFILE = "Bundle_BUNDLE_Entry_NoProfile"; + public static final String BUNDLE_BUNDLE_ENTRY_MULTIPLE_PROFILES = "BUNDLE_BUNDLE_ENTRY_MULTIPLE_PROFILES"; public static final String BUNDLE_BUNDLE_ENTRY_NOTFOUND = "Bundle_BUNDLE_Entry_NotFound"; public static final String BUNDLE_BUNDLE_ENTRY_ORPHAN = "Bundle_BUNDLE_Entry_Orphan"; public static final String BUNDLE_BUNDLE_ENTRY_TYPE = "Bundle_BUNDLE_Entry_Type"; @@ -543,10 +544,14 @@ public class I18nConstants { public static final String VALIDATION_VAL_PROFILE_SIGNPOST_GLOBAL = "VALIDATION_VAL_PROFILE_SIGNPOST_GLOBAL"; public static final String VALIDATION_VAL_PROFILE_SIGNPOST_META = "VALIDATION_VAL_PROFILE_SIGNPOST_META"; public static final String VALIDATION_VAL_PROFILE_SLICEORDER = "Validation_VAL_Profile_SliceOrder"; + public static final String VALIDATION_VAL_PROFILE_OTHER_VERSION = "VALIDATION_VAL_PROFILE_OTHER_VERSION"; + public static final String VALIDATION_VAL_PROFILE_THIS_VERSION_OK = "VALIDATION_VAL_PROFILE_THIS_VERSION_OK"; + public static final String VALIDATION_VAL_PROFILE_THIS_VERSION_OTHER = "VALIDATION_VAL_PROFILE_THIS_VERSION_OTHER"; public static final String VALIDATION_VAL_PROFILE_UNKNOWN = "Validation_VAL_Profile_Unknown"; public static final String VALIDATION_VAL_PROFILE_WRONGTYPE = "Validation_VAL_Profile_WrongType"; public static final String VALIDATION_VAL_PROFILE_WRONGTYPE2 = "Validation_VAL_Profile_WrongType2"; public static final String VALIDATION_VAL_UNKNOWN_PROFILE = "Validation_VAL_Unknown_Profile"; + public static final String VALUESET_INCLUDE_INVALID_CONCEPT_CODE = "VALUESET_INCLUDE_INVALID_CONCEPT_CODE"; public static final String VALUESET_INCLUDE_INVALID_CONCEPT_CODE_VER = "VALUESET_INCLUDE_INVALID_CONCEPT_CODE_VER"; public static final String VALUESET_NO_SYSTEM_WARNING = "VALUESET_NO_SYSTEM_WARNING"; diff --git a/org.hl7.fhir.utilities/src/main/resources/Messages.properties b/org.hl7.fhir.utilities/src/main/resources/Messages.properties index d49720b3c..8bd7307cf 100644 --- a/org.hl7.fhir.utilities/src/main/resources/Messages.properties +++ b/org.hl7.fhir.utilities/src/main/resources/Messages.properties @@ -571,3 +571,7 @@ PACKAGE_VERSION_MISMATCH = FHIR Version mismatch in package {0}: version is {2} VALUESET_REFERENCE_UNKNOWN = The value set import {0} could not be found so cannot be checked VALUESET_REFERENCE_INVALID_TYPE = The value set import {0} points to a resource of type {1} which is not valid SD_MUST_HAVE_DERIVATION = StructureDefinition {0} must have a derivation, since it has a baseDefinition +VALIDATION_VAL_PROFILE_OTHER_VERSION = Profile is for a different version of FHIR ({0}) so has been ignored +VALIDATION_VAL_PROFILE_THIS_VERSION_OK = Profile for this version of FHIR - all OK +VALIDATION_VAL_PROFILE_THIS_VERSION_OTHER = Profile is for this version of FHIR, but is an invalid type {0} +BUNDLE_BUNDLE_ENTRY_MULTIPLE_PROFILES = Multiple profiles found for contained resource. This is not supported at this time. (Type {0}: {1}) diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java index 78ac32bb0..e02dac771 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidationEngine.java @@ -34,6 +34,7 @@ import org.hl7.fhir.r5.utils.*; import org.hl7.fhir.r5.utils.IResourceValidator.*; import org.hl7.fhir.r5.utils.StructureMapUtilities.ITransformerServices; import org.hl7.fhir.utilities.i18n.I18nConstants; +import org.hl7.fhir.validation.cli.services.StandAloneValidatorFetcher.IPackageInstaller; import org.hl7.fhir.validation.instance.InstanceValidator; import org.hl7.fhir.utilities.IniFile; import org.hl7.fhir.utilities.TextFile; @@ -163,7 +164,42 @@ POSSIBILITY OF SUCH DAMAGE. * @author Grahame Grieve * */ -public class ValidationEngine implements IValidatorResourceFetcher { +public class ValidationEngine implements IValidatorResourceFetcher, IPackageInstaller { + + public static class VersionSourceInformation { + + private List report = new ArrayList<>(); + private List versions = new ArrayList<>(); + + public void see(String version, String src) { + version = VersionUtilities.getMajMin(version); + report.add(src+": "+version); + if (!versions.contains(version)) { + versions.add(version); + Collections.sort(versions); + } + } + + public boolean isEmpty() { + return versions.isEmpty(); + } + + public int size() { + return versions.size(); + } + + public String version() { + return versions.get(0); + } + + public List getReport() { + if (report.isEmpty()) { + report.add("(nothing found)"); + } + return report; + } + + } public class ScanOutputItem { private String ref; @@ -311,9 +347,10 @@ public class ValidationEngine implements IValidatorResourceFetcher { public ValidationEngine() throws IOException { pcm = new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION); + context = SimpleWorkerContext.fromNothing(); } - public void setTerminologyServer(String src, String log, FhirPublication version) throws Exception { + public void setTerminologyServer(String src, String log, FhirPublication version) throws FHIRException, URISyntaxException { connectToTSServer(src, log, version); } @@ -342,7 +379,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { this.showTimes = showTimes; } - public ValidationEngine(String src, String txsrvr, String txLog, FhirPublication version, boolean canRunWithoutTerminologyServer, String vString) throws Exception { + public ValidationEngine(String src, String txsrvr, String txLog, FhirPublication version, boolean canRunWithoutTerminologyServer, String vString) throws FHIRException, IOException, URISyntaxException { pcm = new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION); loadCoreDefinitions(src, false); context.setCanRunWithoutTerminology(canRunWithoutTerminologyServer); @@ -350,14 +387,14 @@ public class ValidationEngine implements IValidatorResourceFetcher { this.version = vString; } - public ValidationEngine(String src, String txsrvr, String txLog, FhirPublication version, String vString) throws Exception { + public ValidationEngine(String src, String txsrvr, String txLog, FhirPublication version, String vString) throws FHIRException, IOException, URISyntaxException { pcm = new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION); loadCoreDefinitions(src, false); setTerminologyServer(txsrvr, txLog, version); this.version = vString; } - public ValidationEngine(String src) throws Exception { + public ValidationEngine(String src) throws FHIRException, IOException { loadCoreDefinitions(src, false); pcm = new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION); } @@ -370,7 +407,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { this.language = language; } - private void loadCoreDefinitions(String src, boolean recursive) throws Exception { + private void loadCoreDefinitions(String src, boolean recursive) throws FHIRException, IOException { if (pcm == null) { pcm = new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION); } @@ -435,7 +472,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { return ep; } - private byte[] loadProfileSource(String src) throws Exception { + private byte[] loadProfileSource(String src) throws FHIRException, FileNotFoundException, IOException { if (Utilities.noString(src)) { throw new FHIRException("Profile Source '" + src + "' could not be processed"); } else if (src.startsWith("https:") || src.startsWith("http:")) { @@ -447,13 +484,13 @@ public class ValidationEngine implements IValidatorResourceFetcher { } } - private byte[] loadProfileFromUrl(String src) throws Exception { + private byte[] loadProfileFromUrl(String src) throws FHIRException { try { URL url = new URL(src+"?nocache=" + System.currentTimeMillis()); URLConnection c = url.openConnection(); return IOUtils.toByteArray(c.getInputStream()); } catch (Exception e) { - throw new Exception("Unable to find definitions at URL '"+src+"': "+e.getMessage(), e); + throw new FHIRException("Unable to find definitions at URL '"+src+"': "+e.getMessage(), e); } } @@ -464,9 +501,10 @@ public class ValidationEngine implements IValidatorResourceFetcher { return TextFile.fileToBytes(src); } - /** explore should be true if we're trying to load an -ig parameter, and false if we're loading source **/ + /** explore should be true if we're trying to load an -ig parameter, and false if we're loading source + * @throws IOException **/ - private Map loadIgSource(String src, boolean recursive, boolean explore) throws Exception { + private Map loadIgSource(String src, boolean recursive, boolean explore) throws FHIRException, IOException { // src can be one of the following: // - a canonical url for an ig - this will be converted to a package id and loaded into the cache // - a package id for an ig - this will be loaded into the cache @@ -510,11 +548,60 @@ public class ValidationEngine implements IValidatorResourceFetcher { } else if ((src.matches(FilesystemPackageCacheManager.PACKAGE_REGEX) || src.matches(FilesystemPackageCacheManager.PACKAGE_VERSION_REGEX)) && !src.endsWith(".zip") && !src.endsWith(".tgz")) { return fetchByPackage(src); } - throw new Exception("Unable to find/resolve/read -ig "+src); + throw new FHIRException("Unable to find/resolve/read -ig "+src); + } + + private Map loadIgSourceForVersion(String src, boolean recursive, boolean explore, VersionSourceInformation versions) throws FHIRException, IOException { + if (src.startsWith("https:") || src.startsWith("http:")) { + String v = null; + if (src.contains("|")) { + v = src.substring(src.indexOf("|")+1); + src = src.substring(0, src.indexOf("|")); + } + String pid = pcm.getPackageId(src); + if (!Utilities.noString(pid)) { + versions.see(fetchVersionByPackage(pid+(v == null ? "" : "#"+v)), "Package "+src); + return null; + } else { + return fetchVersionFromUrl(src+(v == null ? "" : "|"+v), explore, versions); + } + } + + File f = new File(Utilities.path(src)); + if (f.exists()) { + if (f.isDirectory() && new File(Utilities.path(src, "package.tgz")).exists()) { + versions.see(loadPackageForVersion(new FileInputStream(Utilities.path(src, "package.tgz")), Utilities.path(src, "package.tgz")), "Package "+src); + return null; + } + if (f.isDirectory() && new File(Utilities.path(src, "igpack.zip")).exists()) + return readZip(new FileInputStream(Utilities.path(src, "igpack.zip"))); + if (f.isDirectory() && new File(Utilities.path(src, "validator.pack")).exists()) + return readZip(new FileInputStream(Utilities.path(src, "validator.pack"))); + if (f.isDirectory()) + return scanDirectory(f, recursive); + if (src.endsWith(".tgz")) { + versions.see(loadPackageForVersion(new FileInputStream(src), src), "Package "+src); + return null; + } + if (src.endsWith(".pack")) + return readZip(new FileInputStream(src)); + if (src.endsWith("igpack.zip")) + return readZip(new FileInputStream(src)); + FhirFormat fmt = checkIsResource(src); + if (fmt != null) { + Map res = new HashMap(); + res.put(Utilities.changeFileExt(src, "."+fmt.getExtension()), TextFile.fileToBytesNCS(src)); + return res; + } + } else if ((src.matches(FilesystemPackageCacheManager.PACKAGE_REGEX) || src.matches(FilesystemPackageCacheManager.PACKAGE_VERSION_REGEX)) && !src.endsWith(".zip") && !src.endsWith(".tgz")) { + versions.see(fetchVersionByPackage(src), "Package "+src); + return null; + } + throw new FHIRException("Unable to find/resolve/read -ig "+src); } - private Map fetchFromUrl(String src, boolean explore) throws Exception { + private Map fetchFromUrl(String src, boolean explore) throws FHIRException, IOException { if (src.endsWith(".tgz")) return loadPackage(fetchFromUrlSpecific(src, false), src); if (src.endsWith(".pack")) @@ -551,15 +638,59 @@ public class ValidationEngine implements IValidatorResourceFetcher { res.put(Utilities.changeFileExt(src, "."+fmt.getExtension()), cnt); return res; } - throw new Exception("Unable to find/resolve/read -ig "+src); + throw new FHIRException("Unable to find/resolve/read -ig "+src); } - private InputStream fetchFromUrlSpecific(String source, boolean optional) throws Exception { + private Map fetchVersionFromUrl(String src, boolean explore, VersionSourceInformation versions) throws FHIRException, IOException { + if (src.endsWith(".tgz")) { + versions.see(loadPackageForVersion(fetchFromUrlSpecific(src, false), src), "From Package "+src); + return null; + } + if (src.endsWith(".pack")) + return readZip(fetchFromUrlSpecific(src, false)); + if (src.endsWith("igpack.zip")) + return readZip(fetchFromUrlSpecific(src, false)); + + InputStream stream = null; + if (explore) { + stream = fetchFromUrlSpecific(Utilities.pathURL(src, "package.tgz"), true); + if (stream != null) { + versions.see(loadPackageForVersion(stream, Utilities.pathURL(src, "package.tgz")), "From Package at "+src); + return null; + } + // todo: these options are deprecated - remove once all IGs have been rebuilt post R4 technical correction + stream = fetchFromUrlSpecific(Utilities.pathURL(src, "igpack.zip"), true); + if (stream != null) + return readZip(stream); + stream = fetchFromUrlSpecific(Utilities.pathURL(src, "validator.pack"), true); + if (stream != null) + return readZip(stream); + stream = fetchFromUrlSpecific(Utilities.pathURL(src, "validator.pack"), true); + //// ----- + } + + // ok, having tried all that... now we'll just try to access it directly + byte[] cnt; + if (stream == null) + cnt = fetchFromUrlSpecific(src, "application/json", true); + else + cnt = TextFile.streamToBytes(stream); + + FhirFormat fmt = checkIsResource(cnt, src); + if (fmt != null) { + Map res = new HashMap(); + res.put(Utilities.changeFileExt(src, "."+fmt.getExtension()), cnt); + return res; + } + throw new FHIRException("Unable to find/resolve/read -ig "+src); + } + + private InputStream fetchFromUrlSpecific(String source, boolean optional) throws FHIRException, IOException { try { URL url = new URL(source+"?nocache=" + System.currentTimeMillis()); URLConnection c = url.openConnection(); return c.getInputStream(); - } catch (Exception e) { + } catch (IOException e) { if (optional) return null; else @@ -567,13 +698,13 @@ public class ValidationEngine implements IValidatorResourceFetcher { } } - private byte[] fetchFromUrlSpecific(String source, String contentType, boolean optional) throws Exception { + private byte[] fetchFromUrlSpecific(String source, String contentType, boolean optional) throws FHIRException, IOException { try { URL url = new URL(source+"?nocache=" + System.currentTimeMillis()); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestProperty("Accept", contentType); return TextFile.streamToBytes(conn.getInputStream()); - } catch (Exception e) { + } catch (IOException e) { if (optional) return null; else @@ -604,11 +735,15 @@ public class ValidationEngine implements IValidatorResourceFetcher { return Utilities.existsInList(Utilities.getFileExtension(ff.getName()).toLowerCase(), "md", "css", "js", "png", "gif", "jpg", "html", "tgz", "pack", "zip"); } - private Map loadPackage(InputStream stream, String name) throws Exception { + private Map loadPackage(InputStream stream, String name) throws FHIRException, IOException { return loadPackage(NpmPackage.fromPackage(stream)); } - public Map loadPackage(NpmPackage pi) throws Exception { + private String loadPackageForVersion(InputStream stream, String name) throws FHIRException, IOException { + return NpmPackage.fromPackage(stream).fhirVersion(); + } + + public Map loadPackage(NpmPackage pi) throws FHIRException, IOException { context.getLoadedPackages().add(pi.name()+"#"+pi.version()); Map res = new HashMap(); for (String s : pi.dependencies()) { @@ -652,7 +787,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { System.out.println(message); } - private Map fetchByPackage(String src) throws Exception { + private Map fetchByPackage(String src) throws FHIRException, IOException { String id = src; String version = null; if (src.contains("#")) { @@ -679,13 +814,46 @@ public class ValidationEngine implements IValidatorResourceFetcher { return loadPackage(pi); } - private Map resolvePackage(String id, String v) throws Exception { + private String fetchVersionByPackage(String src) throws FHIRException, IOException { + String id = src; + String version = null; + if (src.contains("#")) { + id = src.substring(0, src.indexOf("#")); + version = src.substring(src.indexOf("#")+1); + } + if (pcm == null) { + log("Creating Package manager?"); + pcm = new FilesystemPackageCacheManager(true, ToolsVersion.TOOLS_VERSION); + } + if (version == null) { + version = pcm.getLatestVersion(id); + } + NpmPackage pi = null; + if (version == null) { + pi = pcm.loadPackageFromCacheOnly(id); + if (pi != null) + log(" ... Using version "+pi.version()); + } else + pi = pcm.loadPackageFromCacheOnly(id, version); + if (pi == null) { + return resolvePackageForVersion(id, version); + } else { + return pi.fhirVersion(); + } + } + + private Map resolvePackage(String id, String v) throws FHIRException, IOException { NpmPackage pi = pcm.loadPackage(id, v); if (pi != null && v == null) log(" ... Using version "+pi.version()); return loadPackage(pi); } + private String resolvePackageForVersion(String id, String v) throws FHIRException, IOException { + NpmPackage pi = pcm.loadPackage(id, v); + return pi.fhirVersion(); + } + public SimpleWorkerContext getContext() { return context; } @@ -758,7 +926,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { } } - public void loadProfile(String src) throws Exception { + public void loadProfile(String src) throws FHIRException, IOException { if (context.hasResource(StructureDefinition.class, src)) return; if (context.hasResource(ImplementationGuide.class, src)) @@ -770,7 +938,13 @@ public class ValidationEngine implements IValidatorResourceFetcher { context.cacheResource(r); } - public void loadIg(String src, boolean recursive) throws IOException, FHIRException, Exception { + public void scanForIgVersion(String src, boolean recursive, VersionSourceInformation versions) throws IOException, FHIRException, Exception { + Map source = loadIgSourceForVersion(src, recursive, true, versions); + if (source != null && source.containsKey("version.info")) + versions.see(readInfoVersion(source.get("version.info")), "version.info in "+src); + } + + public void loadIg(String src, boolean recursive) throws IOException, FHIRException { NpmPackage npm = src.matches(FilesystemPackageCacheManager.PACKAGE_REGEX) ? pcm.loadPackage(src, null) : null; if (npm != null) { for (String s : npm.dependencies()) { @@ -834,7 +1008,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { return r; } - public Resource loadResourceByVersion(String version, byte[] content, String fn) throws IOException, Exception { + public Resource loadResourceByVersion(String version, byte[] content, String fn) throws IOException, FHIRException { Resource r; if (version.startsWith("3.0")) { org.hl7.fhir.dstu3.model.Resource res; @@ -845,7 +1019,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (fn.endsWith(".txt") || fn.endsWith(".map") ) res = new org.hl7.fhir.dstu3.utils.StructureMapUtilities(null).parse(new String(content)); else - throw new Exception("Unsupported format for "+fn); + throw new FHIRException("Unsupported format for "+fn); r = VersionConvertor_30_50.convertResource(res, false); } else if (version.startsWith("4.0")) { org.hl7.fhir.r4.model.Resource res; @@ -856,7 +1030,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (fn.endsWith(".txt") || fn.endsWith(".map") ) res = new org.hl7.fhir.r4.utils.StructureMapUtilities(null).parse(new String(content), fn); else - throw new Exception("Unsupported format for "+fn); + throw new FHIRException("Unsupported format for "+fn); r = VersionConvertor_40_50.convertResource(res); } else if (version.startsWith("1.4")) { org.hl7.fhir.dstu2016may.model.Resource res; @@ -865,7 +1039,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (fn.endsWith(".json") && !fn.endsWith("template.json")) res = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(new ByteArrayInputStream(content)); else - throw new Exception("Unsupported format for "+fn); + throw new FHIRException("Unsupported format for "+fn); r = VersionConvertor_14_50.convertResource(res); } else if (version.startsWith("1.0")) { org.hl7.fhir.dstu2.model.Resource res; @@ -874,7 +1048,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (fn.endsWith(".json") && !fn.endsWith("template.json")) res = new org.hl7.fhir.dstu2.formats.JsonParser().parse(new ByteArrayInputStream(content)); else - throw new Exception("Unsupported format for "+fn); + throw new FHIRException("Unsupported format for "+fn); VersionConvertorAdvisor50 advisor = new org.hl7.fhir.convertors.misc.IGR2ConvertorAdvisor5(); r = VersionConvertor_10_50.convertResource(res, advisor); } else if (version.equals(Constants.VERSION) || "current".equals(version)) { @@ -887,9 +1061,9 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (fn.endsWith(".txt") || fn.endsWith(".map") ) r = new org.hl7.fhir.r5.utils.StructureMapUtilities(null).parse(new String(content), fn); else - throw new Exception("Unsupported format for "+fn); + throw new FHIRException("Unsupported format for "+fn); } else - throw new Exception("Unsupported version "+version); + throw new FHIRException("Unsupported version "+version); return r; } @@ -923,11 +1097,11 @@ public class ValidationEngine implements IValidatorResourceFetcher { FhirFormat cntType = null; } - public Content loadContent(String source, String opName) throws Exception { + public Content loadContent(String source, String opName) throws FHIRException, IOException { Map s = loadIgSource(source, false, false); Content res = new Content(); if (s.size() != 1) - throw new Exception("Unable to find resource " + source + " to "+opName); + throw new FHIRException("Unable to find resource " + source + " to "+opName); for (Entry t: s.entrySet()) { res.focus = t.getValue(); if (t.getKey().endsWith(".json")) @@ -939,13 +1113,13 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (t.getKey().endsWith(".txt") || t.getKey().endsWith(".map")) res.cntType = FhirFormat.TEXT; else - throw new Exception("Todo: Determining resource type is not yet done"); + throw new FHIRException("Todo: Determining resource type is not yet done"); } return res; } // testing entry point - public OperationOutcome validate(FhirFormat format, InputStream stream, List profiles) throws Exception { + public OperationOutcome validate(FhirFormat format, InputStream stream, List profiles) throws FHIRException, IOException, EOperationOutcome { List messages = new ArrayList(); InstanceValidator validator = getValidator(); validator.validate(null, messages, stream, format, asSdList(profiles)); @@ -966,13 +1140,13 @@ public class ValidationEngine implements IValidatorResourceFetcher { return list; } - public OperationOutcome validate(String source, List profiles) throws Exception { + public OperationOutcome validate(String source, List profiles) throws FHIRException, IOException { List l = new ArrayList(); l.add(source); return (OperationOutcome)validate(l, profiles); } - public List validateScan(List sources, Set guides) throws Exception { + public List validateScan(List sources, Set guides) throws FHIRException, IOException, EOperationOutcome { List refs = new ArrayList(); handleSources(sources, refs); @@ -1048,7 +1222,28 @@ public class ValidationEngine implements IValidatorResourceFetcher { return null; } - public Resource validate(List sources, List profiles) throws Exception { + public void scanForVersions(List sources, VersionSourceInformation versions) throws FHIRException, IOException { + List refs = new ArrayList(); + handleSources(sources, refs); + for (String ref : refs) { + Content cnt = loadContent(ref, "validate"); + String s = TextFile.bytesToString(cnt.focus); + if (s.contains("http://hl7.org/fhir/3.0")) { + versions.see("3.0", "Profile in "+ref); + } + if (s.contains("http://hl7.org/fhir/1.0")) { + versions.see("1.0", "Profile in "+ref); + } + if (s.contains("http://hl7.org/fhir/4.0")) { + versions.see("4.0", "Profile in "+ref); + } + if (s.contains("http://hl7.org/fhir/1.4")) { + versions.see("1.4", "Profile in "+ref); + } + } + } + + public Resource validate(List sources, List profiles) throws FHIRException, IOException { List refs = new ArrayList(); boolean asBundle = handleSources(sources, refs); Bundle results = new Bundle(); @@ -1065,7 +1260,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { results.addEntry().setResource(outcome); } catch (Exception e) { System.out.println("Validation Infrastructure fail validating "+ref+": "+e.getMessage()); - throw e; + throw new FHIRException(e); } } if (asBundle) @@ -1082,7 +1277,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { } } - public OperationOutcome validateString(String location, String source, FhirFormat format, List profiles) throws Exception { + public OperationOutcome validateString(String location, String source, FhirFormat format, List profiles) throws FHIRException, IOException, EOperationOutcome, SAXException { return validate(location, source.getBytes(), format, profiles); } @@ -1136,14 +1331,14 @@ public class ValidationEngine implements IValidatorResourceFetcher { return isBundle; } - public OperationOutcome validate(byte[] source, FhirFormat cntType, List profiles) throws Exception { + public OperationOutcome validate(byte[] source, FhirFormat cntType, List profiles) throws FHIRException, IOException, EOperationOutcome { List messages = new ArrayList(); InstanceValidator validator = getValidator(); validator.validate(null, messages, new ByteArrayInputStream(source), cntType, asSdList(profiles)); return messagesToOutcome(messages); } - public OperationOutcome validate(String location, byte[] source, FhirFormat cntType, List profiles) throws Exception { + public OperationOutcome validate(String location, byte[] source, FhirFormat cntType, List profiles) throws FHIRException, IOException, EOperationOutcome, SAXException { List messages = new ArrayList(); if (doNative) { if (cntType == FhirFormat.JSON) @@ -1161,7 +1356,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { return messagesToOutcome(messages); } - public OperationOutcome validate(String location, byte[] source, FhirFormat cntType, List profiles, IdStatus resourceIdRule, boolean anyExtensionsAllowed, BestPracticeWarningLevel bpWarnings, CheckDisplayOption displayOption) throws Exception { + public OperationOutcome validate(String location, byte[] source, FhirFormat cntType, List profiles, IdStatus resourceIdRule, boolean anyExtensionsAllowed, BestPracticeWarningLevel bpWarnings, CheckDisplayOption displayOption) throws FHIRException, IOException, EOperationOutcome, SAXException { List messages = new ArrayList(); if (doNative) { if (cntType == FhirFormat.JSON) @@ -1254,12 +1449,12 @@ public class ValidationEngine implements IValidatorResourceFetcher { return issue.getSeverity().toString()+" @ "+issue.getLocation() + " " +issue.getDetails().getText() +(source != null ? " (src = "+source+")" : ""); } - public org.hl7.fhir.r5.elementmodel.Element transform(String source, String map) throws Exception { + public org.hl7.fhir.r5.elementmodel.Element transform(String source, String map) throws FHIRException, IOException { Content cnt = loadContent(source, "validate"); return transform(cnt.focus, cnt.cntType, map); } - public org.hl7.fhir.r5.elementmodel.Element transform(byte[] source, FhirFormat cntType, String mapUri) throws Exception { + public org.hl7.fhir.r5.elementmodel.Element transform(byte[] source, FhirFormat cntType, String mapUri) throws FHIRException, IOException { List outputs = new ArrayList(); StructureMapUtilities scu = new StructureMapUtilities(context, new TransformSupportServices(outputs)); @@ -1299,7 +1494,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { return Manager.build(getContext(), structureDefinition); } - public DomainResource generate(String source, String version) throws Exception { + public DomainResource generate(String source, String version) throws FHIRException, IOException, EOperationOutcome { Content cnt = loadContent(source, "validate"); Resource res = loadResourceByVersion(version, cnt.focus, source); RenderingContext rc = new RenderingContext(context, null, null, "http://hl7.org/fhir", "", null, ResourceRendererMode.RESOURCE); @@ -1307,25 +1502,25 @@ public class ValidationEngine implements IValidatorResourceFetcher { return (DomainResource) res; } - public void convert(String source, String output) throws Exception { + public void convert(String source, String output) throws FHIRException, IOException { Content cnt = loadContent(source, "validate"); Element e = Manager.parse(context, new ByteArrayInputStream(cnt.focus), cnt.cntType); Manager.compose(context, e, new FileOutputStream(output), (output.endsWith(".json") ? FhirFormat.JSON : FhirFormat.XML), OutputStyle.PRETTY, null); } - public String evaluateFhirPath(String source, String expression) throws Exception { + public String evaluateFhirPath(String source, String expression) throws FHIRException, IOException { Content cnt = loadContent(source, "validate"); FHIRPathEngine fpe = new FHIRPathEngine(context); Element e = Manager.parse(context, new ByteArrayInputStream(cnt.focus), cnt.cntType); return fpe.evaluateToString(e, expression); } - public StructureDefinition snapshot(String source, String version) throws Exception { + public StructureDefinition snapshot(String source, String version) throws FHIRException, IOException { Content cnt = loadContent(source, "validate"); Resource res = loadResourceByVersion(version, cnt.focus, Utilities.getFileNameForName(source)); if (!(res instanceof StructureDefinition)) - throw new Exception("Require a StructureDefinition for generating a snapshot"); + throw new FHIRException("Require a StructureDefinition for generating a snapshot"); StructureDefinition sd = (StructureDefinition) res; StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); @@ -1670,8 +1865,9 @@ public class ValidationEngine implements IValidatorResourceFetcher { @Override public boolean resolveURL(Object appContext, String path, String url) throws IOException, FHIRException { - if (!url.startsWith("http://hl7.org/fhir")) - return true; // we don't bother with those. + if (!url.startsWith("http://") && !url.startsWith("https://")) { // ignore these + return true; + } if (context.fetchResource(Resource.class, url) != null) return true; if (Utilities.existsInList(url, "http://hl7.org/fhir/sid/us-ssn", "http://hl7.org/fhir/sid/cvx", "http://hl7.org/fhir/sid/ndc", "http://hl7.org/fhir/sid/us-npi", "http://hl7.org/fhir/sid/icd-10", @@ -1679,6 +1875,9 @@ public class ValidationEngine implements IValidatorResourceFetcher { "http://hl7.org/fhir/workflow", "http://hl7.org/fhir/ConsentPolicy/opt-out", "http://hl7.org/fhir/ConsentPolicy/opt-in")) { return true; } + if (Utilities.existsInList(url, "http://loinc.org", "http://unitsofmeasure.org", "http://snomed.info/sct")) { + return true; + } if (fetcher != null) { return fetcher.resolveURL(appContext, path, url); }; @@ -1690,7 +1889,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { this.locale = locale; } - public void handleOutput(Resource r, String output, String version) throws Exception { + public void handleOutput(Resource r, String output, String version) throws FHIRException, IOException { if (output.startsWith("http://") || output.startsWith("http://")) { ByteArrayOutputStream bs = new ByteArrayOutputStream(); handleOutputToStream(r, output, bs, version); @@ -1719,7 +1918,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { } } - private void handleOutputToStream(Resource r, String fn, OutputStream s, String version) throws Exception { + private void handleOutputToStream(Resource r, String fn, OutputStream s, String version) throws FHIRException, IOException { if (fn.endsWith(".html") || fn.endsWith(".htm") && r instanceof DomainResource) new XhtmlComposer(XhtmlComposer.HTML, true).compose(s, ((DomainResource) r).getText().getDiv()); else if (version.startsWith("3.0")) { @@ -1731,7 +1930,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (fn.endsWith(".txt") || fn.endsWith(".map") ) TextFile.stringToStream(org.hl7.fhir.dstu3.utils.StructureMapUtilities.render((org.hl7.fhir.dstu3.model.StructureMap) res), s, false); else - throw new Exception("Unsupported format for "+fn); + throw new FHIRException("Unsupported format for "+fn); } else if (version.startsWith("4.0")) { org.hl7.fhir.r4.model.Resource res = VersionConvertor_40_50.convertResource(r); if (fn.endsWith(".xml") && !fn.endsWith("template.xml")) @@ -1741,7 +1940,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (fn.endsWith(".txt") || fn.endsWith(".map") ) TextFile.stringToStream(org.hl7.fhir.r4.utils.StructureMapUtilities.render((org.hl7.fhir.r4.model.StructureMap) res), s, false); else - throw new Exception("Unsupported format for "+fn); + throw new FHIRException("Unsupported format for "+fn); } else if (version.startsWith("1.4")) { org.hl7.fhir.dstu2016may.model.Resource res = VersionConvertor_14_50.convertResource(r); if (fn.endsWith(".xml") && !fn.endsWith("template.xml")) @@ -1749,7 +1948,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (fn.endsWith(".json") && !fn.endsWith("template.json")) new org.hl7.fhir.dstu2016may.formats.JsonParser().setOutputStyle(org.hl7.fhir.dstu2016may.formats.IParser.OutputStyle.PRETTY).compose(s, res); else - throw new Exception("Unsupported format for "+fn); + throw new FHIRException("Unsupported format for "+fn); } else if (version.startsWith("1.0")) { VersionConvertorAdvisor50 advisor = new org.hl7.fhir.convertors.misc.IGR2ConvertorAdvisor5(); org.hl7.fhir.dstu2.model.Resource res = VersionConvertor_10_50.convertResource(r, advisor); @@ -1758,7 +1957,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (fn.endsWith(".json") && !fn.endsWith("template.json")) new org.hl7.fhir.dstu2.formats.JsonParser().setOutputStyle(org.hl7.fhir.dstu2.formats.IParser.OutputStyle.PRETTY).compose(s, res); else - throw new Exception("Unsupported format for "+fn); + throw new FHIRException("Unsupported format for "+fn); } else if (version.equals(Constants.VERSION)) { if (fn.endsWith(".xml") && !fn.endsWith("template.xml")) new XmlParser().setOutputStyle(org.hl7.fhir.r5.formats.IParser.OutputStyle.PRETTY).compose(s, r); @@ -1767,9 +1966,9 @@ public class ValidationEngine implements IValidatorResourceFetcher { else if (fn.endsWith(".txt") || fn.endsWith(".map") ) TextFile.stringToStream(org.hl7.fhir.r5.utils.StructureMapUtilities.render((org.hl7.fhir.r5.model.StructureMap) r), s, false); else - throw new Exception("Unsupported format for "+fn); + throw new FHIRException("Unsupported format for "+fn); } else - throw new Exception("Encounted unsupported configured version "+version+" loading "+fn); + throw new FHIRException("Encounted unsupported configured version "+version+" loading "+fn); s.close(); } @@ -1833,7 +2032,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { } else if (VersionUtilities.isR4Ver(version)) { return convertVersionNativeR4(targetVer, cnt, format); } else { - throw new Exception("Source version not supported yet: "+version); + throw new FHIRException("Source version not supported yet: "+version); } } catch (Exception e) { System.out.println("Conversion failed using Java convertor: "+e.getMessage()); @@ -1940,7 +2139,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { throw new FHIRException("Unsupported output format: "+cnt.cntType.toString()); } } else { - throw new Exception("Target Version not supported yet: "+targetVer); + throw new FHIRException("Target Version not supported yet: "+targetVer); } } @@ -2009,7 +2208,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { throw new FHIRException("Unsupported output format: "+cnt.cntType.toString()); } } else { - throw new Exception("Target Version not supported yet: "+targetVer); + throw new FHIRException("Target Version not supported yet: "+targetVer); } } @@ -2078,7 +2277,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { throw new FHIRException("Unsupported output format: "+cnt.cntType.toString()); } } else { - throw new Exception("Target Version not supported yet: "+targetVer); + throw new FHIRException("Target Version not supported yet: "+targetVer); } } @@ -2147,7 +2346,7 @@ public class ValidationEngine implements IValidatorResourceFetcher { throw new FHIRException("Unsupported output format: "+cnt.cntType.toString()); } } else { - throw new Exception("Target Version not supported yet: "+targetVer); + throw new FHIRException("Target Version not supported yet: "+targetVer); } } @@ -2165,5 +2364,10 @@ public class ValidationEngine implements IValidatorResourceFetcher { } } + + public FilesystemPackageCacheManager getPcm() { + return pcm; + } + } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/Validator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/Validator.java index c903ac741..8ea219d5b 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/Validator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/Validator.java @@ -65,6 +65,7 @@ import org.hl7.fhir.r5.model.ImplementationGuide; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.validation.ValidationEngine.VersionSourceInformation; import org.hl7.fhir.validation.cli.ValidatorGui; import org.hl7.fhir.validation.cli.services.ComparisonService; import org.hl7.fhir.validation.cli.services.ValidationService; @@ -171,6 +172,10 @@ public class Validator { Display.printCliArgumentsAndInfo(args); cliContext = Params.loadCliContext(args); + if (cliContext.getSv() == null) { + cliContext.setSv(determineVersion(cliContext)); + } + // Comment this out because definitions filename doesn't necessarily contain version (and many not even be 14 characters long). Version gets spit out a couple of lines later after we've loaded the context String definitions = VersionUtilities.packageForVersion(cliContext.getSv()) + "#" + VersionUtilities.getCurrentVersion(cliContext.getSv()); ValidationEngine validator = ValidationService.getValidator(cliContext, definitions); @@ -204,5 +209,27 @@ public class Validator { } } } + } + + public static String determineVersion(CliContext cliContext) throws Exception { + if (cliContext.getMode() != EngineMode.VALIDATION) { + return "current"; + } + System.out.println("Scanning for versions (no -version parameter):"); + VersionSourceInformation versions = ValidationService.scanForVersions(cliContext); + for (String s : versions.getReport()) { + System.out.println(" "+s); + } + if (versions.isEmpty()) { + System.out.println("-> Using Default version '"+VersionUtilities.CURRENT_VERSION+"'"); + return "current"; + } + if (versions.size() == 1) { + System.out.println("-> use version "+versions.version()); + return versions.version(); + } + throw new Exception("-> Multiple versions found. Specify a particular version using the -version parameter"); + } + } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java index 668629e25..1b0bda203 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/model/CliContext.java @@ -36,7 +36,7 @@ public class CliContext { @JsonProperty("txServer") private String txServer = "http://tx.fhir.org"; @JsonProperty("sv") - private String sv = "current"; + private String sv = null; @JsonProperty("txLog") private String txLog = null; @JsonProperty("mapLog") diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java new file mode 100644 index 000000000..90e24d387 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/StandAloneValidatorFetcher.java @@ -0,0 +1,109 @@ +package org.hl7.fhir.validation.cli.services; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Locale; + +import org.hl7.fhir.exceptions.DefinitionException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.utils.IResourceValidator.IValidatorResourceFetcher; +import org.hl7.fhir.r5.utils.IResourceValidator.ReferenceValidationPolicy; +import org.hl7.fhir.utilities.Utilities; +import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.VersionUtilities.VersionURLInfo; +import org.hl7.fhir.utilities.cache.BasePackageCacheManager; +import org.hl7.fhir.utilities.cache.FilesystemPackageCacheManager; +import org.hl7.fhir.utilities.cache.NpmPackage; +import org.hl7.fhir.validation.cli.services.StandAloneValidatorFetcher.IPackageInstaller; + +public class StandAloneValidatorFetcher implements IValidatorResourceFetcher { + + public interface IPackageInstaller { + public void loadIg(String src, boolean recursive) throws IOException, FHIRException; + } + + private BasePackageCacheManager pcm; + private IWorkerContext context; + private IPackageInstaller installer; + + public StandAloneValidatorFetcher(FilesystemPackageCacheManager pcm, IWorkerContext context, IPackageInstaller installer) { + super(); + this.pcm = pcm; + this.context = context; + this.installer = installer; + } + + @Override + public Element fetch(Object appContext, String url) throws FHIRFormatError, DefinitionException, FHIRException, IOException { + throw new Error("Not done yet"); + } + + @Override + public ReferenceValidationPolicy validationPolicy(Object appContext, String path, String url) { + throw new Error("Not done yet"); + } + + @Override + public boolean resolveURL(Object appContext, String path, String url) throws IOException, FHIRException { + if (!Utilities.isAbsoluteUrl(url)) { + return false; + } + // 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 + + // first possibility: it's a reference to a version specific URL http://hl7.org/fhir/X.X/... + VersionURLInfo vu = VersionUtilities.parseVersionUrl(url); + if (vu != null) { + NpmPackage pi = pcm.loadPackage(VersionUtilities.packageForVersion(vu.getVersion()), VersionUtilities.getCurrentVersion(vu.getVersion())); + return pi.hasCanonical(vu.getUrl()); + } + + // ok maybe it's a reference to a package we know + String base = findBaseUrl(url); + String pid = pcm.getPackageId(base); + if (url.contains("|")) { + pid = pid+"#"+url.substring(url.indexOf("|")+1); + } + if (pid != null) { + installer.loadIg(pid, false); + NpmPackage pi = pcm.loadPackage(pid); + return pi.hasCanonical(url); + } + + if (!url.startsWith("http://hl7.org/fhir")) { + return true; // we don't bother with those in the standalone validator - we assume they are valid + } + + // we assume it's invalid at this point + return false; + + } + + private String findBaseUrl(String url) { + String[] p = url.split("\\/"); + for (int i = 1; i< p.length; i++) { + if (Utilities.existsInList(p[i], context.getResourceNames())) { + StringBuilder b = new StringBuilder(p[0]); + for (int j = 1; j < i; j++) { + b.append("/"); + b.append(p[j]); + } + return b.toString(); + } + } + return null; + } + + @Override + public byte[] fetchRaw(String url) throws MalformedURLException, IOException { + throw new Error("Not done yet"); + } + + @Override + public void setLocale(Locale locale) { + throw new Error("Not done yet"); + } + +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java index 08c9e4741..9c331c052 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java @@ -11,6 +11,7 @@ import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.validation.ValidationEngine; +import org.hl7.fhir.validation.ValidationEngine.VersionSourceInformation; import org.hl7.fhir.validation.cli.model.*; import java.io.File; @@ -52,6 +53,15 @@ public class ValidationService { return response; } + public static VersionSourceInformation scanForVersions(CliContext cliContext) throws Exception { + VersionSourceInformation versions = new VersionSourceInformation(); + ValidationEngine ve = new ValidationEngine(); + for (String src : cliContext.getIgs()) { + ve.scanForIgVersion(src, cliContext.isRecursive(), versions); + } + ve.scanForVersions(cliContext.getSources(), versions); + return versions; + } public static void validateSources(CliContext cliContext, ValidationEngine validator, long loadStart) throws Exception { validator.doneLoading(loadStart); if (cliContext.getProfiles().size() > 0) { @@ -205,6 +215,7 @@ public class ValidationService { validator.setSecurityChecks(cliContext.isSecurityChecks()); validator.setCrumbTrails(cliContext.isCrumbTrails()); validator.setShowTimes(cliContext.isShowTimes()); + validator.setFetcher(new StandAloneValidatorFetcher(validator.getPcm(), validator.getContext(), validator)); TerminologyCache.setNoCaching(cliContext.isNoInternalCaching()); return validator; } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java index 3b72413a2..b2d890072 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/instance/InstanceValidator.java @@ -151,6 +151,7 @@ import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.Utilities.DecimalStatus; import org.hl7.fhir.utilities.VersionUtilities; +import org.hl7.fhir.utilities.VersionUtilities.VersionURLInfo; import org.hl7.fhir.utilities.validation.ValidationOptions; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; @@ -592,8 +593,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat throw new FHIRException(e1); } timeTracker.load(t); - if (e != null) + if (e != null) { validate(appContext, errors, e, profiles); + } return e; } @@ -1856,7 +1858,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (fetcher != null) { boolean found; try { - found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com"))) || (url.startsWith("http://hl7.org/fhir/tools")) || fetcher.resolveURL(appContext, path, url); + found = isDefinitionURL(url) || (allowExamples && (url.contains("example.org") || url.contains("acme.com")) || url.contains("acme.org")) || (url.startsWith("http://hl7.org/fhir/tools")) || fetcher.resolveURL(appContext, path, url); } catch (IOException e1) { found = false; } @@ -2928,7 +2930,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return rr; } } - if (stack.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY) { + if (stack.getElement().getSpecial() == SpecialElement.BUNDLE_ENTRY || stack.getElement().getSpecial() == SpecialElement.PARAMETER) { return null; // we don't try to resolve contained references across this boundary } stack = stack.getParent(); @@ -2963,6 +2965,18 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return rr; } } + if (stack.getElement().getSpecial() == SpecialElement.PARAMETER && stack.getParent() != null) { + NodeStack tgt = findInParams(stack.getParent().getParent(), ref); + if (tgt != null) { + ResolvedReference rr = new ResolvedReference(); + rr.setResource(tgt.getElement()); + rr.setFocus(tgt.getElement()); + rr.setExternal(false); + rr.setStack(tgt); + rr.getStack().qualifyPath(".ofType("+tgt.getElement().fhirType()+")"); + return rr; + } + } stack = stack.getParent(); } // we can get here if we got called via FHIRPath conformsTo which breaks the stack continuity. @@ -2989,6 +3003,42 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat return null; } + private NodeStack findInParams(NodeStack params, String ref) { + int i = 0; + for (Element child : params.getElement().getChildren("parameter")) { + NodeStack p = params.push(child, i, child.getProperty().getDefinition(), child.getProperty().getDefinition()); + if (child.hasChild("resource")) { + Element res = child.getNamedChild("resource"); + if ((res.fhirType()+"/"+res.getIdBase()).equals(ref)) { + return p.push(res, -1, res.getProperty().getDefinition(), res.getProperty().getDefinition()); + } + } + NodeStack pc = findInParamParts(p, child, ref); + if (pc != null) { + return pc; + } + } + return null; + } + + private NodeStack findInParamParts(NodeStack pp, Element param, String ref) { + int i = 0; + for (Element child : param.getChildren("part")) { + NodeStack p = pp.push(child, i, child.getProperty().getDefinition(), child.getProperty().getDefinition()); + if (child.hasChild("resource")) { + Element res = child.getNamedChild("resource"); + if ((res.fhirType()+"/"+res.getIdBase()).equals(ref)) { + return p.push(res, -1, res.getProperty().getDefinition(), res.getProperty().getDefinition()); + } + } + NodeStack pc = findInParamParts(p, child, ref); + if (pc != null) { + return pc; + } + } + return null; + } + private Element getEntryForSource(Element bundle, Element element) { List entries = new ArrayList(); bundle.getNamedChildren(ENTRY, entries); @@ -3432,7 +3482,18 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat for (Element profile : profiles) { StructureDefinition sd = context.fetchResource(StructureDefinition.class, profile.primitiveValue()); if (!defn.getUrl().equals(profile.primitiveValue())) { - if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", sd != null, I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN, profile.primitiveValue())) { + // is this a version specific reference? + VersionURLInfo vu = VersionUtilities.parseVersionUrl(profile.primitiveValue()); + if (vu != null) { + if (!VersionUtilities.versionsCompatible(vu.getVersion(), context.getVersion())) { + hint(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", false, I18nConstants.VALIDATION_VAL_PROFILE_OTHER_VERSION, vu.getVersion()); + } else if (vu.getUrl().equals(defn.getUrl())) { + hint(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", false, I18nConstants.VALIDATION_VAL_PROFILE_THIS_VERSION_OK); + } else { + StructureDefinition sdt = context.fetchResource(StructureDefinition.class, vu.getUrl()); + rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", false, I18nConstants.VALIDATION_VAL_PROFILE_THIS_VERSION_OTHER, sdt == null ? "null" : sdt.getType()); + } + } else if (warning(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath() + ".meta.profile[" + i + "]", sd != null, I18nConstants.VALIDATION_VAL_PROFILE_UNKNOWN, profile.primitiveValue())) { signpost(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), !crumbTrails, I18nConstants.VALIDATION_VAL_PROFILE_SIGNPOST_META, sd.getUrl()); stack.resetIds(); startInner(hostContext, errors, resource, element, sd, stack, false); @@ -3658,9 +3719,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (trr == null) { rule(errors, IssueType.INFORMATIONAL, element.line(), element.col(), stack.getLiteralPath(), false, I18nConstants.BUNDLE_BUNDLE_ENTRY_TYPE, resourceName); } else if (isValidResourceType(resourceName, trr)) { - long t = System.nanoTime(); - StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); - timeTracker.sd(t); // special case: resource wrapper is reset if we're crossing a bundle boundary, but not otherwise ValidatorHostContext hc = null; if (element.getSpecial() == SpecialElement.BUNDLE_ENTRY || element.getSpecial() == SpecialElement.BUNDLE_OUTCOME || element.getSpecial() == SpecialElement.PARAMETER) { @@ -3669,10 +3727,29 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } else { hc = hostContext.forContained(element); } - trackUsage(profile, hostContext, element); stack.resetIds(); - if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), profile != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOPROFILE, resourceName)) { - validateResource(hc, errors, resource, element, profile, idstatus, stack); + if (trr.getProfile().size() == 1) { + long t = System.nanoTime(); + StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, trr.getProfile().get(0).asStringValue()); + timeTracker.sd(t); + trackUsage(profile, hostContext, element); + if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), profile != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOPROFILE, resourceName)) { + validateResource(hc, errors, resource, element, profile, idstatus, stack); + } + } else if (trr.getProfile().size() == 0) { + long t = System.nanoTime(); + StructureDefinition profile = this.context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); + timeTracker.sd(t); + trackUsage(profile, hostContext, element); + if (rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), profile != null, I18nConstants.BUNDLE_BUNDLE_ENTRY_NOPROFILE, resourceName)) { + validateResource(hc, errors, resource, element, profile, idstatus, stack); + } + } else { + CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); + for (CanonicalType u : trr.getProfile()) { + b.append(u.asStringValue()); + } + rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), false, I18nConstants.BUNDLE_BUNDLE_ENTRY_MULTIPLE_PROFILES, trr.getCode(), b.toString()); } } else { List types = new ArrayList<>();