diff --git a/.vscode/launch.json b/.vscode/launch.json index 42d82af1fc4..5aab0a5a5de 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -67,6 +67,14 @@ "cwd": "${workspaceFolder}/matchbox-server" }, { + "type": "java", + "name": "Launch Matchbox-Server (test)", + "request": "launch", + "mainClass": "ca.uhn.fhir.jpa.starter.Application", + "projectName": "matchbox-server", + "vmArgs": "-Dspring.config.additional-location=file:/Users/oegger/Documents/github/matchbox/matchbox-server/target/test-classes/application-test.yaml", + "cwd": "${workspaceFolder}/matchbox-server" + }, { "type": "java", "name": "Launch Matchbox-Server", "request": "launch", diff --git a/docs/changelog.md b/docs/changelog.md index f6a0b240382..4e378fea91d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,9 @@ +2024/02/xx Release 3.6.0 + +- TODO: `docker pull europe-west6-docker.pkg.dev/ahdis-ch/ahdis/matchbox:v3.6.0` +- Upgraded to HAPI FHIR 7.0.0 and org.hl7.fhir.core 6.1.2.2 [#191](https://github.com/ahdis/matchbox/issues/191) +- added matchbox validation API tests [#193](https://github.com/ahdis/matchbox/issues/193) + 2024/01/31 Release 3.5.4 - `docker pull europe-west6-docker.pkg.dev/ahdis-ch/ahdis/matchbox:v3.5.4` diff --git a/matchbox-server/pom.xml b/matchbox-server/pom.xml index e74e1311cfb..456ff4d1873 100644 --- a/matchbox-server/pom.xml +++ b/matchbox-server/pom.xml @@ -135,11 +135,46 @@ h2 + + + org.testcontainers + testcontainers + test + + + org.testcontainers + elasticsearch + test + + + org.testcontainers + junit-jupiter + test + + ca.uhn.hapi.fhir hapi-fhir-client + + org.hl7.fhir.testcases + fhir-test-cases + test + + + + ca.uhn.hapi.fhir + hapi-fhir-test-utilities + test + + + + org.awaitility + awaitility + test + + org.springframework.boot spring-boot-autoconfigure @@ -162,6 +197,27 @@ javax.xml.bind jaxb-api + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.springframework.boot + spring-boot-starter-test + test + @@ -169,7 +225,7 @@ matchbox - + org.apache.maven.plugins @@ -190,10 +246,33 @@ true + + + ca.uhn.hapi.fhir + hapi-fhir-testpage-overlay + + false + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + + + + + integration-test + verify + + + + + org.basepom.maven duplicate-finder-maven-plugin @@ -261,6 +340,9 @@ src/main/resources + + src/test/resources + ${project.build.directory}/generated-sources/properties false diff --git a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ValidationProvider.java b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ValidationProvider.java index 17227b52d1d..1b889688c8f 100644 --- a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ValidationProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ValidationProvider.java @@ -41,8 +41,13 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; +import org.hl7.fhir.r5.model.DateTimeType; +import org.hl7.fhir.r5.model.DateType; +import org.hl7.fhir.r5.model.Duration; import org.hl7.fhir.r5.model.OperationOutcome; import org.hl7.fhir.r5.model.StringType; +import org.hl7.fhir.r5.model.TimeType; +import org.hl7.fhir.r5.model.UriType; import org.hl7.fhir.r5.utils.OperationOutcomeUtilities; import org.hl7.fhir.r5.utils.ToolingExtensions; import org.hl7.fhir.utilities.validation.ValidationMessage; @@ -73,9 +78,6 @@ public class ValidationProvider { @Autowired private FhirContext myContext; - @Autowired - private INpmPackageVersionDao myPackageVersionDao; - // @Operation(name = "$canonical", manualRequest = true, idempotent = true, returnParameters = { // @OperationParam(name = "return", type = IBase.class, min = 1, max = 1) }) // public IBaseResource canonical(HttpServletRequest theRequest) { @@ -183,9 +185,10 @@ public IBaseResource validate(final HttpServletRequest theRequest) { return this.getOoForError("Error during validation: %s".formatted(e.getMessage())); } - sw.endCurrentTask(); + long millis = sw.getMillis(); log.debug("Validation time: {}", sw); - return this.getOperationOutcome(sha3Hex, messages, profile, engine, sw.formatTaskDurations(), cliContext); + + return this.getOperationOutcome(sha3Hex, messages, profile, engine, millis, cliContext); } private String getContentString(final HttpServletRequest theRequest, @@ -219,7 +222,7 @@ private IBaseResource getOperationOutcome(final String id, final List messages, final String profile, final MatchboxEngine engine, - final String taskDuration, + final long ms, final CliContext cliContext) { final var oo = new OperationOutcome(); oo.setId(id); @@ -231,6 +234,9 @@ private IBaseResource getOperationOutcome(final String id, issue.setCode(OperationOutcome.IssueType.INFORMATIONAL); final StructureDefinition structDef = engine.getStructureDefinition(profile); + + final org.hl7.fhir.r5.model.StructureDefinition structDefR5 = (org.hl7.fhir.r5.model.StructureDefinition) VersionConvertorFactory_40_50.convertResource(structDef); + final var profileDate = (structDef.getDateElement() != null) ? " (%s)".formatted(structDef.getDateElement().asStringValue()) : " "; @@ -241,10 +247,28 @@ private IBaseResource getOperationOutcome(final String id, structDef.getVersion(), profileDate, String.join(", ", engine.getContext().getLoadedPackages()), - taskDuration, + "" + ms/1000.0+ "s", VersionUtil.getPoweredBy(), cliContext.toString() )); + + var ext = issue.addExtension().setUrl("http://matchbox.health/validiation"); + ext.addExtension("profile", new UriType(structDef.getUrl())); + ext.addExtension("profileVersion", new UriType(structDef.getVersion())); + ext.addExtension("profileDate", structDefR5.getDateElement()); + + ext.addExtension("total", new Duration().setUnit("ms").setValue(ms) ); + if (matchboxEngineSupport.getSessionId(engine) != null) { + ext.addExtension("validatorVersion", new StringType(VersionUtil.getPoweredBy())); + } + cliContext.addContextToExtension(ext); + if (matchboxEngineSupport.getSessionId(engine) != null) { + ext.addExtension("sessionId", new StringType(matchboxEngineSupport.getSessionId(engine))); + } + for(String pkg : engine.getContext().getLoadedPackages()) { + ext.addExtension("package", new StringType(pkg)); + } + } // Map the SingleValidationMessages to OperationOutcomeIssue diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java index e5675f0e16c..7a62c234781 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java @@ -10,6 +10,11 @@ import java.util.stream.Collectors; import org.apache.commons.beanutils.BeanUtils; +import org.apache.jena.sparql.function.library.e; +import org.hl7.fhir.r5.model.BooleanType; +import org.hl7.fhir.r5.model.Extension; +import org.hl7.fhir.r5.model.StringType; +import org.hl7.fhir.r5.model.UriType; import org.hl7.fhir.r5.terminologies.JurisdictionUtilities; import org.hl7.fhir.r5.utils.validation.BundleValidationRule; import org.hl7.fhir.utilities.VersionUtilities; @@ -28,15 +33,16 @@ /** * A POJO for storing the flags/values for the CLI validator. - * Needed to copy the class because the setters with CliContext as return type are not accessible via reflection - * In addition we have parameters from the CliContext which do not make sense to expose for the Web APi + * Needed to copy the class because the setters with CliContext as return type + * are not accessible via reflection + * In addition we have parameters from the CliContext which do not make sense to + * expose for the Web APi */ @Component public class CliContext { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CliContext.class); - @JsonProperty("doNative") private boolean doNative = false; @JsonProperty("hintAboutNonMustSupport") @@ -51,8 +57,9 @@ public class CliContext { private boolean assumeValidRestReferences = false; @JsonProperty("canDoNative") private boolean canDoNative = false; -// @JsonProperty("noInternalCaching") -// private boolean noInternalCaching = false; // internal, for when debugging terminology validation + // @JsonProperty("noInternalCaching") + // private boolean noInternalCaching = false; // internal, for when debugging + // terminology validation @JsonProperty("noExtensibleBindingMessages") private boolean noExtensibleBindingMessages = false; @JsonProperty("noUnicodeBiDiControlChars") @@ -101,12 +108,12 @@ public class CliContext { // private List igs = new ArrayList(); @JsonProperty("ig") private String ig = null; - + @JsonProperty("questionnaire") private QuestionnaireMode questionnaireMode = QuestionnaireMode.CHECK; @JsonProperty("level") private ValidationLevel level = ValidationLevel.HINTS; - + // @JsonProperty("profiles") // private List profiles = new ArrayList(); // @JsonProperty("sources") @@ -117,19 +124,19 @@ public class CliContext { @JsonProperty("securityChecks") private boolean securityChecks = false; - + @JsonProperty("crumbTrails") private boolean crumbTrails = false; - + @JsonProperty("forPublication") private boolean forPublication = false; - + @JsonProperty("allowExampleUrls") private boolean allowExampleUrls = false; - + // @JsonProperty("showTimes") // private boolean showTimes = false; - + @JsonProperty("locale") private String locale = Locale.ENGLISH.getDisplayLanguage(); @@ -138,7 +145,7 @@ public class CliContext { // @JsonProperty("outputStyle") // private String outputStyle = null; - + // TODO: Mark what goes here? // private List bundleValidationRules = new ArrayList<>(); @@ -159,31 +166,32 @@ public boolean getOnlyOneEngine() { private boolean httpReadOnly = false; - public boolean isHttpReadOnly() { - return this.httpReadOnly; + public boolean isHttpReadOnly() { + return this.httpReadOnly; } @Autowired public CliContext(Environment environment) { - // get al list of all JsonProperty of cliContext with return values property name and property type - List cliContextProperties = getValidateEngineParameters(); + // get al list of all JsonProperty of cliContext with return values property + // name and property type + List cliContextProperties = getValidateEngineParameters(); - // check for each cliContextProperties if it is in the request parameter - for (Field field : cliContextProperties) { - String cliContextProperty = field.getName(); + // check for each cliContextProperties if it is in the request parameter + for (Field field : cliContextProperties) { + String cliContextProperty = field.getName(); String value = environment.getProperty("matchbox.fhir.context." + cliContextProperty); - if (value!=null && value.length()>0) { - try { - if (field.getType() == boolean.class) { - BeanUtils.setProperty(this, cliContextProperty, Boolean.parseBoolean(value)); - } else { - BeanUtils.setProperty(this, cliContextProperty, value); - } - } catch (IllegalAccessException | InvocationTargetException e) { - log.error("error setting property " + cliContextProperty + " to " + value); - } - } - } + if (value != null && value.length() > 0) { + try { + if (field.getType() == boolean.class) { + BeanUtils.setProperty(this, cliContextProperty, Boolean.parseBoolean(value)); + } else { + BeanUtils.setProperty(this, cliContextProperty, value); + } + } catch (IllegalAccessException | InvocationTargetException e) { + log.error("error setting property " + cliContextProperty + " to " + value); + } + } + } // get properties array from the environment? this.igsPreloaded = environment.getProperty("matchbox.fhir.context.igsPreloaded", String[].class); this.onlyOneEngine = environment.getProperty("matchbox.fhir.context.onlyOneEngine", Boolean.class, false); @@ -192,54 +200,53 @@ public CliContext(Environment environment) { public CliContext(CliContext other) { List cliContextProperties = getValidateEngineParameters(); - // check for each cliContextProperties if it is in the request parameter - for (Field field : cliContextProperties) { + // check for each cliContextProperties if it is in the request parameter + for (Field field : cliContextProperties) { try { String value = BeanUtils.getProperty(other, field.getName()); - if (value!=null) { - BeanUtils.setProperty(this, field.getName(), value); - } + if (value != null) { + BeanUtils.setProperty(this, field.getName(), value); + } + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + log.error("error setting property " + field.getName()); } - catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - log.error("error setting property " + field.getName() ); - } - } + } this.igsPreloaded = other.igsPreloaded; this.onlyOneEngine = other.onlyOneEngine; - this.httpReadOnly = other.httpReadOnly; + this.httpReadOnly = other.httpReadOnly; } @JsonProperty("ig") public String getIg() { - return ig; + return ig; } @JsonProperty("igs") public void setIg(String ig) { - this.ig = ig; + this.ig = ig; } // @JsonProperty("igs") // public void setIgs(List igs) { - // this.igs = igs; + // this.igs = igs; // } // @JsonProperty("igs") // public List getIgs() { - // return igs; + // return igs; // } // @JsonProperty("igs") // public void setIgs(List igs) { - // this.igs = igs; + // this.igs = igs; // } // public CliContext addIg(String ig) { - // if (this.igs == null) { - // this.igs = new ArrayList<>(); - // } - // this.igs.add(ig); - // return this; + // if (this.igs == null) { + // this.igs = new ArrayList<>(); + // } + // this.igs.add(ig); + // return this; // } @JsonProperty("questionnaire") @@ -377,7 +384,6 @@ public CliContext addLocation(String profile, String location) { return this; } - @JsonProperty("lang") public String getLang() { return lang; @@ -390,16 +396,26 @@ public void setLang(String lang) { @JsonProperty("snomedCT") public String getSnomedCT() { - if ("intl".equals(snomedCT)) return "900000000000207008"; - if ("us".equals(snomedCT)) return "731000124108"; - if ("uk".equals(snomedCT)) return "999000041000000102"; - if ("au".equals(snomedCT)) return "32506021000036107"; - if ("ca".equals(snomedCT)) return "20611000087101"; - if ("nl".equals(snomedCT)) return "11000146104"; - if ("se".equals(snomedCT)) return "45991000052106"; - if ("es".equals(snomedCT)) return "449081005"; - if ("dk".equals(snomedCT)) return "554471000005108"; - if ("ch".equals(snomedCT)) return "2011000195101"; + if ("intl".equals(snomedCT)) + return "900000000000207008"; + if ("us".equals(snomedCT)) + return "731000124108"; + if ("uk".equals(snomedCT)) + return "999000041000000102"; + if ("au".equals(snomedCT)) + return "32506021000036107"; + if ("ca".equals(snomedCT)) + return "20611000087101"; + if ("nl".equals(snomedCT)) + return "11000146104"; + if ("se".equals(snomedCT)) + return "45991000052106"; + if ("es".equals(snomedCT)) + return "449081005"; + if ("dk".equals(snomedCT)) + return "554471000005108"; + if ("ch".equals(snomedCT)) + return "2011000195101"; return snomedCT; } @@ -440,12 +456,12 @@ public void setAssumeValidRestReferences(boolean assumeValidRestReferences) { // @JsonProperty("noInternalCaching") // public boolean isNoInternalCaching() { - // return noInternalCaching; + // return noInternalCaching; // } // @JsonProperty("noInternalCaching") // public void setNoInternalCaching(boolean noInternalCaching) { - // this.noInternalCaching = noInternalCaching; + // this.noInternalCaching = noInternalCaching; // } @JsonProperty("noExtensibleBindingMessages") @@ -457,7 +473,7 @@ public boolean isNoExtensibleBindingMessages() { public void setNoExtensibleBindingMessages(boolean noExtensibleBindingMessages) { this.noExtensibleBindingMessages = noExtensibleBindingMessages; } - + @JsonProperty("noInvariants") public boolean isNoInvariants() { return noInvariants; @@ -488,12 +504,12 @@ public void setWantInvariantsInMessages(boolean wantInvariantsInMessages) { this.wantInvariantsInMessages = wantInvariantsInMessages; } - @JsonProperty("securityChecks") + @JsonProperty("securityChecks") public boolean isSecurityChecks() { return securityChecks; } - @JsonProperty("securityChecks") + @JsonProperty("securityChecks") public void setSecurityChecks(boolean securityChecks) { this.securityChecks = securityChecks; } @@ -543,125 +559,164 @@ public void setJurisdiction(String jurisdiction) { this.jurisdiction = jurisdiction; } - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (!(o instanceof final CliContext that)) return false; - return doNative == that.doNative - && hintAboutNonMustSupport == that.hintAboutNonMustSupport - && recursive == that.recursive - && showMessagesFromReferences == that.showMessagesFromReferences - && doDebug == that.doDebug - && assumeValidRestReferences == that.assumeValidRestReferences - && canDoNative == that.canDoNative - && noExtensibleBindingMessages == that.noExtensibleBindingMessages - && noUnicodeBiDiControlChars == that.noUnicodeBiDiControlChars - && noInvariants == that.noInvariants - && displayIssuesAreWarnings == that.displayIssuesAreWarnings - && wantInvariantsInMessages == that.wantInvariantsInMessages - && doImplicitFHIRPathStringConversion == that.doImplicitFHIRPathStringConversion - && securityChecks == that.securityChecks - && crumbTrails == that.crumbTrails - && forPublication == that.forPublication - && allowExampleUrls == that.allowExampleUrls - && onlyOneEngine == that.onlyOneEngine - && httpReadOnly == that.httpReadOnly - && htmlInMarkdownCheck == that.htmlInMarkdownCheck - && Objects.equals(txServer, that.txServer) - && Objects.equals(lang, that.lang) - && Objects.equals(snomedCT, that.snomedCT) - && Objects.equals(fhirVersion, that.fhirVersion) - && Objects.equals(ig, that.ig) - && questionnaireMode == that.questionnaireMode - && level == that.level - && mode == that.mode - && Objects.equals(locale, that.locale) - && Objects.equals(locations, that.locations) - && Objects.equals(jurisdiction, that.jurisdiction) - && Arrays.equals(igsPreloaded, that.igsPreloaded); - } - - @Override - public int hashCode() { - int result = Objects.hash(doNative, - hintAboutNonMustSupport, - recursive, - showMessagesFromReferences, - doDebug, - assumeValidRestReferences, - canDoNative, - noExtensibleBindingMessages, - noUnicodeBiDiControlChars, - noInvariants, - displayIssuesAreWarnings, - wantInvariantsInMessages, - doImplicitFHIRPathStringConversion, - htmlInMarkdownCheck, - txServer, - lang, - snomedCT, - fhirVersion, - ig, - questionnaireMode, - level, - mode, - securityChecks, - crumbTrails, - forPublication, - allowExampleUrls, - locale, - locations, - jurisdiction, - onlyOneEngine, - httpReadOnly); - result = 31 * result + Arrays.hashCode(igsPreloaded); - return result; - } - - @Override - public String toString() { - return "CliContext{" + - "doNative=" + doNative + - ", hintAboutNonMustSupport=" + hintAboutNonMustSupport + - ", recursive=" + recursive + - ", showMessagesFromReferences=" + showMessagesFromReferences + - ", doDebug=" + doDebug + - ", assumeValidRestReferences=" + assumeValidRestReferences + - ", canDoNative=" + canDoNative + - ", noExtensibleBindingMessages=" + noExtensibleBindingMessages + - ", noUnicodeBiDiControlChars=" + noUnicodeBiDiControlChars + - ", noInvariants=" + noInvariants + - ", displayIssuesAreWarnings=" + displayIssuesAreWarnings + - ", wantInvariantsInMessages=" + wantInvariantsInMessages + - ", doImplicitFHIRPathStringConversion=" + doImplicitFHIRPathStringConversion + - ", htmlInMarkdownCheck=" + htmlInMarkdownCheck + - ", txServer='" + txServer + '\'' + - ", lang='" + lang + '\'' + - ", snomedCT='" + snomedCT + '\'' + - ", fhirVersion='" + fhirVersion + '\'' + - ", ig='" + ig + '\'' + - ", questionnaireMode=" + questionnaireMode + - ", level=" + level + - ", mode=" + mode + - ", securityChecks=" + securityChecks + - ", crumbTrails=" + crumbTrails + - ", forPublication=" + forPublication + - ", allowExampleUrls=" + allowExampleUrls + - ", locale='" + locale + '\'' + - ", locations=" + locations + - ", jurisdiction='" + jurisdiction + '\'' + - ", igsPreloaded=" + Arrays.toString(igsPreloaded) + - ", onlyOneEngine=" + onlyOneEngine + - ", httpReadOnly=" + httpReadOnly + - '}'; - } - - public List getValidateEngineParameters() { - List cliContextProperties = Arrays.asList(this.getClass().getDeclaredFields()).stream() - .filter(f -> f.isAnnotationPresent(JsonProperty.class)) - .filter(f -> f.getName() != "profile") - .filter(f -> f.getType() == String.class || f.getType() == boolean.class) - .collect(Collectors.toList()); - return cliContextProperties; - } + @Override + public boolean equals(final Object o) { + if (this == o) + return true; + if (!(o instanceof final CliContext that)) + return false; + return doNative == that.doNative + && hintAboutNonMustSupport == that.hintAboutNonMustSupport + && recursive == that.recursive + && showMessagesFromReferences == that.showMessagesFromReferences + && doDebug == that.doDebug + && assumeValidRestReferences == that.assumeValidRestReferences + && canDoNative == that.canDoNative + && noExtensibleBindingMessages == that.noExtensibleBindingMessages + && noUnicodeBiDiControlChars == that.noUnicodeBiDiControlChars + && noInvariants == that.noInvariants + && displayIssuesAreWarnings == that.displayIssuesAreWarnings + && wantInvariantsInMessages == that.wantInvariantsInMessages + && doImplicitFHIRPathStringConversion == that.doImplicitFHIRPathStringConversion + && securityChecks == that.securityChecks + && crumbTrails == that.crumbTrails + && forPublication == that.forPublication + && allowExampleUrls == that.allowExampleUrls + && onlyOneEngine == that.onlyOneEngine + && httpReadOnly == that.httpReadOnly + && htmlInMarkdownCheck == that.htmlInMarkdownCheck + && Objects.equals(txServer, that.txServer) + && Objects.equals(lang, that.lang) + && Objects.equals(snomedCT, that.snomedCT) + && Objects.equals(fhirVersion, that.fhirVersion) + && Objects.equals(ig, that.ig) + && questionnaireMode == that.questionnaireMode + && level == that.level + && mode == that.mode + && Objects.equals(locale, that.locale) + && Objects.equals(locations, that.locations) + && Objects.equals(jurisdiction, that.jurisdiction) + && Arrays.equals(igsPreloaded, that.igsPreloaded); + } + + @Override + public int hashCode() { + int result = Objects.hash(doNative, + hintAboutNonMustSupport, + recursive, + showMessagesFromReferences, + doDebug, + assumeValidRestReferences, + canDoNative, + noExtensibleBindingMessages, + noUnicodeBiDiControlChars, + noInvariants, + displayIssuesAreWarnings, + wantInvariantsInMessages, + doImplicitFHIRPathStringConversion, + securityChecks, + crumbTrails, + forPublication, + httpReadOnly, + allowExampleUrls, + htmlInMarkdownCheck, + txServer, + lang, + snomedCT, + fhirVersion, + ig, + questionnaireMode, + level, + mode, + locale, + locations, + jurisdiction); + result = 31 * result + Arrays.hashCode(igsPreloaded); + return result; + } + + @Override + public String toString() { + return "CliContext{" + + "doNative=" + doNative + + ", hintAboutNonMustSupport=" + hintAboutNonMustSupport + + ", recursive=" + recursive + + ", showMessagesFromReferences=" + showMessagesFromReferences + + ", doDebug=" + doDebug + + ", assumeValidRestReferences=" + assumeValidRestReferences + + ", canDoNative=" + canDoNative + + ", noExtensibleBindingMessages=" + noExtensibleBindingMessages + + ", noUnicodeBiDiControlChars=" + noUnicodeBiDiControlChars + + ", noInvariants=" + noInvariants + + ", displayIssuesAreWarnings=" + displayIssuesAreWarnings + + ", wantInvariantsInMessages=" + wantInvariantsInMessages + + ", doImplicitFHIRPathStringConversion=" + doImplicitFHIRPathStringConversion + + ", htmlInMarkdownCheck=" + htmlInMarkdownCheck + + ", txServer='" + txServer + '\'' + + ", lang='" + lang + '\'' + + ", snomedCT='" + snomedCT + '\'' + + ", fhirVersion='" + fhirVersion + '\'' + + ", ig='" + ig + '\'' + + ", questionnaireMode=" + questionnaireMode + + ", level=" + level + + ", mode=" + mode + + ", securityChecks=" + securityChecks + + ", crumbTrails=" + crumbTrails + + ", forPublication=" + forPublication + + ", allowExampleUrls=" + allowExampleUrls + + ", locale='" + locale + '\'' + + ", locations=" + locations + + ", jurisdiction='" + jurisdiction + '\'' + + ", igsPreloaded=" + Arrays.toString(igsPreloaded) + + ", onlyOneEngine=" + onlyOneEngine + + ", httpReadOnly=" + httpReadOnly + + '}'; + } + + public List getValidateEngineParameters() { + List cliContextProperties = Arrays.asList(this.getClass().getDeclaredFields()).stream() + .filter(f -> f.isAnnotationPresent(JsonProperty.class)) + .filter(f -> f.getName() != "profile") + .filter(f -> f.getType() == String.class || f.getType() == boolean.class) + .collect(Collectors.toList()); + return cliContextProperties; + } + + public void addContextToExtension(Extension ext) { + + ext.addExtension("ig", new StringType(ig)); + ext.addExtension("hintAboutNonMustSupport", new BooleanType(hintAboutNonMustSupport)); + ext.addExtension("recursive", new BooleanType(recursive)); + + ext.addExtension("showMessagesFromReferences", new BooleanType(showMessagesFromReferences)); + ext.addExtension("doDebug", new BooleanType(doDebug)); + ext.addExtension("assumeValidRestReferences", new BooleanType(assumeValidRestReferences)); + ext.addExtension("canDoNative", new BooleanType(canDoNative)); + ext.addExtension("noExtensibleBindingMessages", new BooleanType(noExtensibleBindingMessages)); + ext.addExtension("noUnicodeBiDiControlChars", new BooleanType(noUnicodeBiDiControlChars)); + ext.addExtension("noInvariants", new BooleanType(noInvariants)); + ext.addExtension("displayIssuesAreWarnings", new BooleanType(displayIssuesAreWarnings)); + ext.addExtension("wantInvariantsInMessages", new BooleanType(wantInvariantsInMessages)); + ext.addExtension("doImplicitFHIRPathStringConversion", new BooleanType(doImplicitFHIRPathStringConversion)); + // ext.addExtension("htmlInMarkdownCheck", new BooleanType(htmlInMarkdownCheck + // == HtmlInMarkdownCheck.ERROR)); + + ext.addExtension("securityChecks", new BooleanType(securityChecks)); + ext.addExtension("crumbTrails", new BooleanType(crumbTrails)); + ext.addExtension("forPublication", new BooleanType(forPublication)); + ext.addExtension("httpReadOnly", new BooleanType(httpReadOnly)); + ext.addExtension("allowExampleUrls", new BooleanType(allowExampleUrls)); + ext.addExtension("txServer", new UriType(txServer)); + ext.addExtension("lang", new StringType(lang)); + ext.addExtension("snomedCT", new BooleanType(snomedCT)); + ext.addExtension("fhirVersion", new StringType(fhirVersion)); + ext.addExtension("ig", new StringType(ig)); +// ext.addExtension("questionnaireMode", new BooleanType(questionnaireMode)); +// ext.addExtension("level", new BooleanType(level)); + // ext.addExtension("mode", new BooleanType(mode)); + ext.addExtension("locale", new StringType(locale)); +// ext.addExtension("locations", new StringType(locations)); + ext.addExtension("jurisdiction", new StringType(jurisdiction)); + + } } diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/MatchboxEngineSupport.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/MatchboxEngineSupport.java index d905e88ce65..e57d1a95335 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/MatchboxEngineSupport.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/MatchboxEngineSupport.java @@ -401,6 +401,10 @@ public MatchboxEngine getMatchboxEngineNotSynchronized(final @Nullable String ca return null; } + public String getSessionId(final MatchboxEngine engine) { + return this.sessionCache.getSessionId(engine); + } + public boolean isInitialized() { return initialized; } diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/util/EngineSessionCache.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/EngineSessionCache.java index 763229aa23d..cd203f5021d 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/util/EngineSessionCache.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/EngineSessionCache.java @@ -21,10 +21,10 @@ package ch.ahdis.matchbox.util; import java.util.Set; -import java.util.UUID; import java.util.HashSet; import java.util.Map; +import org.apache.commons.collections4.map.PassiveExpiringMap; import org.hl7.fhir.validation.ValidationEngine; import org.hl7.fhir.validation.cli.services.SessionCache; @@ -32,11 +32,17 @@ * @author Oliver Egger * * We want to have a validation engines also that are not timed out as - * in the parent classe. + * in the parent class. */ public class EngineSessionCache extends SessionCache { private final Map cachedSessionsNoTimeout = new java.util.HashMap(); + private final Map cachedSessionIdsNoTimeout = new java.util.HashMap(); + private final PassiveExpiringMap cachedSessionIds; + + public EngineSessionCache() { + cachedSessionIds = new PassiveExpiringMap<>(TIME_TO_LIVE, TIME_UNIT); + } /** * Returns the stored {@link ValidationEngine} associated with the passed in @@ -80,6 +86,13 @@ public Set getSessionIds() { */ public String cacheSessionForEver(String sessionId, ValidationEngine validationEngine) { cachedSessionsNoTimeout.put(sessionId, validationEngine); + cachedSessionIdsNoTimeout.put(validationEngine, sessionId); + return sessionId; + } + + public String cacheSession(String sessionId, ValidationEngine validationEngine) { + String id = super.cacheSession(sessionId, validationEngine); + cachedSessionIds.put(validationEngine, id); return sessionId; } @@ -88,4 +101,14 @@ public boolean sessionExists(String sessionId) { return super.sessionExists(sessionId) || cachedSessionsNoTimeout.containsKey(sessionId); } + public String getSessionId(ValidationEngine validationEngine) { + if (cachedSessionIdsNoTimeout.containsKey(validationEngine)) { + return cachedSessionIdsNoTimeout.get(validationEngine); + } + if (cachedSessionIds.containsKey(validationEngine)) { + return cachedSessionIds.get(validationEngine); + } + return null; + } + } diff --git a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/GenericFhirClient.java b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/GenericFhirClient.java new file mode 100644 index 00000000000..78d6871e34f --- /dev/null +++ b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/GenericFhirClient.java @@ -0,0 +1,163 @@ +package ch.ahdis.matchbox.test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.method.*; +import org.apache.commons.io.IOUtils; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import com.google.common.base.Charsets; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; +import ca.uhn.fhir.rest.client.impl.GenericClient; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import org.hl7.fhir.r4.model.CapabilityStatement; + + +/** + * ValidationClient extends the Generic Client + * @author oliveregger + * + */ +public class GenericFhirClient extends GenericClient{ + + static final public String testServer = "http://localhost:8080/matchboxv3/fhir"; + + public GenericFhirClient(FhirContext theContext, String theServerBase) { + super(theContext, null, theServerBase, null); + setDontValidateConformance(true); + theContext.getRestfulClientFactory().setSocketTimeout(600 * 1000); + } + + public GenericFhirClient(FhirContext theContext) { + this(theContext, testServer); + } + + private final class OutcomeResponseHandler implements IClientResponseHandler { + + private OutcomeResponseHandler() { + super(); + } + + @Override + public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { + MethodOutcome response = MethodUtil.process2xxResponse(getFhirContext(), theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders); + response.setCreatedUsingStatusCode(theResponseStatusCode); + response.setResponseHeaders(theHeaders); + return response; + } + } + + public static BaseHttpClientInvocation createOperationInvocation(FhirContext theContext, String theOperationName, String theInput, Map> urlParams) { + StringBuilder b = new StringBuilder(); + if (b.length() > 0) { + b.append('/'); + } + if (!theOperationName.startsWith("$")) { + b.append("$"); + } + b.append(theOperationName); + BaseHttpClientInvocation.appendExtraParamsWithQuestionMark(urlParams, b, b.indexOf("?") == -1); + return new HttpPostClientInvocation(theContext, theInput, false, b.toString()); + } + + /** + * Performs the $validate operation with a direct POST (see http://hl7.org/fhir/resource-operation-validate.html#examples) + * and the profile specified as a parameter (not the Parameters syntact). + * @param theContents content to validate + * @param theProfile optional: profile to validate against + * @return + */ + public IBaseOperationOutcome validate(String theContents, String theProfile) { + setEncoding(EncodingEnum.detectEncoding(theContents)); + Map> theExtraParams = null; + if (theProfile!=null) { + theExtraParams = new HashMap>(); + List profiles = new ArrayList(); + profiles.add(theProfile); + theExtraParams.put("profile", profiles); + } + OutcomeResponseHandler binding = new OutcomeResponseHandler(); + BaseHttpClientInvocation clientInvoke = createOperationInvocation(getFhirContext(), "$validate", theContents, theExtraParams); + MethodOutcome resp = invokeClient(getFhirContext(), binding, clientInvoke, null, null, false, null, null, null, null, null); + return resp.getOperationOutcome(); + } + + + private String getStructureMapTransformOperation(Map> urlParams) { + StringBuilder b = new StringBuilder(); + b.append("StructureMap/$transform"); + BaseHttpClientInvocation.appendExtraParamsWithQuestionMark(urlParams, b, b.indexOf("?") == -1); + return b.toString(); + } + + public BaseHttpClientInvocation createStructureMapTransformInvocation(FhirContext theContext, String theInput, Map> urlParams) { + return new HttpPostClientInvocation(theContext, theInput, false, getStructureMapTransformOperation(urlParams)); + } + + public BaseHttpClientInvocation createStructureMapTransformInvocation(FhirContext theContext, IBaseResource resource, Map> urlParams) { + return new HttpPostClientInvocation(theContext, resource, getStructureMapTransformOperation(urlParams)); + } + + + /** + * Performs the $transform operation with a direct POST and returning a Resource + * @return + */ + public IBaseResource convert(String theContents, EncodingEnum contentEndoding, String sourceMapUrl, String acceptHeader) { + setEncoding(contentEndoding); + Map> theExtraParams = null; + if (sourceMapUrl!=null) { + theExtraParams = new HashMap>(); + List urls = new ArrayList(); + urls.add(sourceMapUrl); + theExtraParams.put("source", urls); + } + ResourceResponseHandler binding = new ResourceResponseHandler(); + BaseHttpClientInvocation clientInvoke = createStructureMapTransformInvocation(getFhirContext(), theContents, theExtraParams); + return invokeClient(getFhirContext(), binding, clientInvoke, null, null, false, null, null, null, acceptHeader, null); + } + + private final class StringResponseHandler implements IClientResponseHandler { + + @Override + public String invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) + throws IOException, BaseServerResponseException { + return IOUtils.toString(theResponseInputStream, Charsets.UTF_8); + } + } + + /** + * Performs the $transform operation with a direct POST and returning a Resource + * @return + */ + public String convert(IBaseResource resource, EncodingEnum contentEndoding, String sourceMapUrl, String acceptHeader) { + setEncoding(contentEndoding); + Map> theExtraParams = null; + if (sourceMapUrl!=null) { + theExtraParams = new HashMap>(); + List urls = new ArrayList(); + urls.add(sourceMapUrl); + theExtraParams.put("source", urls); + } + BaseHttpClientInvocation clientInvoke = createStructureMapTransformInvocation(getFhirContext(), resource, theExtraParams); + return invokeClient(getFhirContext(), new StringResponseHandler(), clientInvoke, null, null, false, null, null, null, acceptHeader, null); + } + + public CapabilityStatement retrieveCapabilityStatement() { + IGenericClient client = getFhirContext().newRestfulGenericClient(testServer); + CapabilityStatement capabilityStatement = client.capabilities().ofType(CapabilityStatement.class).execute(); + return capabilityStatement; + } + +} diff --git a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiTest.java b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiTest.java new file mode 100644 index 00000000000..94c86457ec8 --- /dev/null +++ b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/MatchboxApiTest.java @@ -0,0 +1,265 @@ +package ch.ahdis.matchbox.test; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.FileUtils; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseExtension; +import org.hl7.fhir.instance.model.api.IBaseHasExtensions; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity; +import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; +import org.hl7.fhir.r4.model.Parameters; +import org.junit.jupiter.api.BeforeAll; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.jpa.starter.Application; + +import static org.junit.Assert.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT) +@ContextConfiguration(classes = { Application.class }) +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class MatchboxApiTest { + + static public int getValidationFailures(OperationOutcome outcome) { + int fails = 0; + if (outcome != null && outcome.getIssue() != null) { + for (OperationOutcomeIssueComponent issue : outcome.getIssue()) { + if (IssueSeverity.FATAL == issue.getSeverity()) { + ++fails; + } + if (IssueSeverity.ERROR == issue.getSeverity()) { + ++fails; + } + } + } + return fails; + } + + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MatchboxApiTest.class); + + private String targetServer = "http://localhost:8081/matchboxv3/fhir"; + + @BeforeAll + void waitUntilStartup() throws Exception { + Path dir = Paths.get("database"); + if (Files.exists(dir)) { + for (Path file : Files.list(dir).collect(Collectors.toList())) { + if (Files.isRegularFile(file)) { + Files.delete(file); + } + } + } + Thread.sleep(10000); // give the server some time to start up + FhirContext contextR4 = FhirVersionEnum.R4.newContext(); + ValidationClient validationClient = new ValidationClient(contextR4, this.targetServer); + validationClient.capabilities(); + } + + private static IBaseExtension getMatchboxValidationExtension(FhirContext theCtx, + IBaseOperationOutcome theOutcome) { + if (theOutcome == null) { + return null; + } + RuntimeResourceDefinition ooDef = theCtx.getResourceDefinition(theOutcome); + BaseRuntimeChildDefinition issueChild = ooDef.getChildByName("issue"); + List issues = issueChild.getAccessor().getValues(theOutcome); + if (issues.isEmpty()) { + return null; + } + IBase issue = issues.get(0); + if (issue instanceof IBaseHasExtensions) { + List> extensions = ((IBaseHasExtensions) issue).getExtension(); + for (IBaseExtension nextSource : extensions) { + if (nextSource.getUrl().equals("http://matchbox.health/validiation")) { + return nextSource; + } + } + } + return null; + } + + public String getSessionId(FhirContext ctx, IBaseOperationOutcome outcome) { + IBaseExtension ext = getMatchboxValidationExtension(ctx, outcome); + List> extensions = (List>) ext.getExtension(); + for (IBaseExtension next : extensions) { + if (next.getUrl().equals("sessionId")) { + IPrimitiveType value = (IPrimitiveType) next.getValue(); + return value.getValueAsString(); + } + } + return null; + } + + public String getIg(FhirContext ctx, IBaseOperationOutcome outcome) { + IBaseExtension ext = getMatchboxValidationExtension(ctx, outcome); + List> extensions = (List>) ext.getExtension(); + for (IBaseExtension next : extensions) { + if (next.getUrl().equals("ig")) { + IPrimitiveType value = (IPrimitiveType) next.getValue(); + return value.getValueAsString(); + } + } + return null; + } + + public String getTxServer(FhirContext ctx, IBaseOperationOutcome outcome) { + IBaseExtension ext = getMatchboxValidationExtension(ctx, outcome); + List> extensions = (List>) ext.getExtension(); + for (IBaseExtension next : extensions) { + if (next.getUrl().equals("txServer")) { + IPrimitiveType value = (IPrimitiveType) next.getValue(); + return value.getValueAsString(); + } + } + return null; + } + + @Test + public void validatePatientRawR4() { + FhirContext contextR4 = FhirVersionEnum.R4.newContext(); + ValidationClient validationClient = new ValidationClient(contextR4, this.targetServer); + + String patient = "\n" + " \n" + + " \n" + " \n" + + "
42
\n" + "
\n" + + "
\n"; + + IBaseOperationOutcome operationOutcome = validationClient.validate(patient, + "http://hl7.org/fhir/StructureDefinition/Patient"); + assertEquals(0, getValidationFailures((OperationOutcome) operationOutcome)); + + String sessionIdFirst = getSessionId(contextR4, operationOutcome); + + operationOutcome = validationClient.validate(patient, + "http://hl7.org/fhir/StructureDefinition/Bundle"); + assertEquals(1, getValidationFailures((OperationOutcome) operationOutcome)); + + String sessionIdF2nd = getSessionId(contextR4, operationOutcome); + assertEquals(sessionIdFirst,sessionIdF2nd); + } + + @Test + public void verifyCachingImplementationGuides() { + FhirContext contextR4 = FhirVersionEnum.R4.newContext(); + ValidationClient validationClient = new ValidationClient(contextR4, this.targetServer); + + String resource = "\n" + // + " \n" + // + " \n" + // + " \n" + // + " \n" + // + ""; + + // tests against base core profile + String profileCore = "http://hl7.org/fhir/StructureDefinition/Practitioner"; + IBaseOperationOutcome operationOutcome = validationClient.validate(resource, profileCore); + String sessionIdCore = getSessionId(contextR4, operationOutcome); + assertEquals(0, getValidationFailures((OperationOutcome) operationOutcome)); + assertEquals("hl7.fhir.r4.core#4.0.1", getIg(contextR4, operationOutcome)); + assertEquals("http://localhost:8081/matchboxv3/fhir", this.getTxServer(contextR4, operationOutcome)); + + // tests against matchbox r4 test ig + String profileMatchbox = "http://matchbox.health/ig/test/r4/StructureDefinition/practitioner-identifier-required"; + operationOutcome = validationClient.validate(resource, profileMatchbox); + String sessionIdMatchbox = getSessionId(contextR4, operationOutcome); + assertEquals(0, getValidationFailures((OperationOutcome) operationOutcome)); + assertEquals("matchbox.health.test.ig.r4#0.1.0", getIg(contextR4, operationOutcome)); + + // verify that we have have different validation engine + assertNotEquals(sessionIdCore, sessionIdMatchbox); + + // check that the cached validation engine of core gets used + operationOutcome = validationClient.validate(resource, profileCore); + assertEquals(0, getValidationFailures((OperationOutcome) operationOutcome)); + String sessionId2Core = getSessionId(contextR4, operationOutcome); + assertEquals(sessionIdCore, sessionId2Core); + + // check that the cached validation engine of matchbox r4 test ig is used again + operationOutcome = validationClient.validate(resource, profileMatchbox); + assertEquals(0, getValidationFailures((OperationOutcome) operationOutcome)); + String sessionId2Matchbox = getSessionId(contextR4, operationOutcome); + assertEquals(sessionIdMatchbox, sessionId2Matchbox); + + // add new parameters should create a new validation engine for matchbox r4 test ig + Parameters parameters = new Parameters(); + parameters.addParameter("txServer", "n/a"); + operationOutcome = validationClient.validate(resource, profileMatchbox, parameters); + assertEquals(0, getValidationFailures((OperationOutcome) operationOutcome)); + String sessionId2MatchboxTxNa = getSessionId(contextR4, operationOutcome); + assertNotEquals(sessionIdMatchbox, sessionId2MatchboxTxNa); + assertEquals("matchbox.health.test.ig.r4#0.1.0", getIg(contextR4, operationOutcome)); + assertEquals("n/a", this.getTxServer(contextR4, operationOutcome)); + + // add new parameters should create a new validation engine for default validation + // operationOutcome = validationClient.validate(resource, profileCore); + // String sessionId3CoreTxNa = getSessionId(contextR4, operationOutcome); + // assertEquals(0, getValidationFailures((OperationOutcome) operationOutcome)); + // assertNotEquals(sessionIdCore, sessionId3CoreTxNa); + } + + @Test + // https://gazelle.ihe.net/jira/browse/EHS-431 + public void validateEhs431() throws IOException { + // + FhirContext contextR4 = FhirVersionEnum.R4.newContext(); + ValidationClient validationClient = new ValidationClient(contextR4, this.targetServer); + + validationClient.capabilities(); + + // IBaseOperationOutcome operationOutcome = + // validationClient.validate(getContent("ehs-431.json"), + // "http://fhir.ch/ig/ch-emed/StructureDefinition/ch-emed-document-medicationcard"); + IBaseOperationOutcome operationOutcome = validationClient.validate(getContent("ehs-431.json"), + "http://hl7.org/fhir/StructureDefinition/Bundle"); + log.debug(contextR4.newJsonParser().encodeResourceToString(operationOutcome)); + assertEquals(1, getValidationFailures((OperationOutcome) operationOutcome)); + } + + @Test + // https://gazelle.ihe.net/jira/browse/EHS-419 + public void validateEhs419() throws IOException { + // + FhirContext contextR4 = FhirVersionEnum.R4.newContext(); + ValidationClient validationClient = new ValidationClient(contextR4, this.targetServer); + + validationClient.capabilities(); + + IBaseOperationOutcome operationOutcome = validationClient.validate(getContent("ehs-419.json"), + "http://hl7.org/fhir/StructureDefinition/Patient"); + log.debug(contextR4.newJsonParser().encodeResourceToString(operationOutcome)); + assertEquals(0, getValidationFailures((OperationOutcome) operationOutcome)); + } + + private String getContent(String resourceName) throws IOException { + Resource resource = new ClassPathResource(resourceName); + File file = resource.getFile(); + return FileUtils.readFileToString(file, StandardCharsets.UTF_8); + } + +} diff --git a/matchbox-server/src/test/java/ch/ahdis/matchbox/test/ValidationClient.java b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/ValidationClient.java new file mode 100644 index 00000000000..83790b40a26 --- /dev/null +++ b/matchbox-server/src/test/java/ch/ahdis/matchbox/test/ValidationClient.java @@ -0,0 +1,138 @@ +package ch.ahdis.matchbox.test; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; +import ca.uhn.fhir.rest.client.impl.GenericClient; +import ca.uhn.fhir.rest.client.method.HttpPostClientInvocation; +import ca.uhn.fhir.rest.client.method.IClientResponseHandler; +import ca.uhn.fhir.rest.client.method.MethodUtil; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.util.ParametersUtil; + + +/** + * ValidationClient extends the GenericClient + * @author oliveregger + * + */ +public class ValidationClient extends GenericClient{ + + + public ValidationClient(FhirContext theContext, String theServerBase) { + super(theContext, null, theServerBase, null); + setDontValidateConformance(true); + theContext.getRestfulClientFactory().setSocketTimeout(600 * 1000); + } + + private final class OutcomeResponseHandler implements IClientResponseHandler { + + private OutcomeResponseHandler() { + super(); + } + + @Override + public MethodOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map> theHeaders) throws BaseServerResponseException { + MethodOutcome response = MethodUtil.process2xxResponse(getFhirContext(), theResponseStatusCode, theResponseMimeType, theResponseInputStream, theHeaders); + response.setCreatedUsingStatusCode(theResponseStatusCode); + response.setResponseHeaders(theHeaders); + return response; + } + } + + public static BaseHttpClientInvocation createValidationInvocation(FhirContext theContext, String theOperationName, String theInput, Map> urlParams) { + StringBuilder b = new StringBuilder(); + if (b.length() > 0) { + b.append('/'); + } + if (!theOperationName.startsWith("$")) { + b.append("$"); + } + b.append(theOperationName); + BaseHttpClientInvocation.appendExtraParamsWithQuestionMark(urlParams, b, b.indexOf("?") == -1); + return new HttpPostClientInvocation(theContext, theInput, false, b.toString()); + } + + private static Optional> getNameValue( + IBase nextParameter, BaseRuntimeElementCompositeDefinition theNextParameterDef) { + BaseRuntimeChildDefinition nameChild = theNextParameterDef.getChildByName("name"); + List nameValues = nameChild.getAccessor().getValues(nextParameter); + return nameValues.stream() + .filter(t -> t instanceof IPrimitiveType) + .map(t -> ((IPrimitiveType) t)) + .findFirst(); + } + + + private List getParameterNamesFirstLevel(FhirContext theCtx, IBaseParameters theParameters) { + RuntimeResourceDefinition resDef = theCtx.getResourceDefinition(theParameters.getClass()); + BaseRuntimeChildDefinition parameterChild = resDef.getChildByName("parameter"); + List parameterReps = parameterChild.getAccessor().getValues(theParameters); + if (parameterReps!=null) + return parameterReps.stream() + .map(t -> getNameValue(t, (BaseRuntimeElementCompositeDefinition) theCtx.getElementDefinition(t.getClass())).get().getValueAsString()).toList(); + return null; + } + + /** + * Performs the $validate operation with a direct POST (see http://hl7.org/fhir/resource-operation-validate.html#examples) + * and the profile specified as a parameter, additional parameters can be provided + * @param theContents content to validate + * @param theProfile optional: profile to validate against + * @return + */ + public IBaseOperationOutcome validate(String theContents, String theProfile) { + return validate(theContents, theProfile, null); + } + + + /** + * Performs the $validate operation with a direct POST (see http://hl7.org/fhir/resource-operation-validate.html#examples) + * and the profile specified as a parameter, additional parameters can be provided + * @param theContents content to validate + * @param theProfile optional: profile to validate against + * @param parameters optional: additional validation parameters + * @return + */ + public IBaseOperationOutcome validate(String theContents, String theProfile, IBaseParameters parameters) { + setEncoding(EncodingEnum.detectEncoding(theContents)); + Map> theExtraParams = null; + if (theProfile!=null || parameters!=null) { + theExtraParams = new HashMap>(); + if (theProfile != null) { + List profiles = new ArrayList(); + profiles.add(theProfile); + theExtraParams.put("profile", profiles); + } + if (parameters!=null) { + List parametersLevel1 = getParameterNamesFirstLevel(getFhirContext(), parameters); + if (parametersLevel1 != null) { + for (String parameter : parametersLevel1) { + theExtraParams.put(parameter, ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), parameters, parameter)); + } + } + } + } + OutcomeResponseHandler binding = new OutcomeResponseHandler(); + BaseHttpClientInvocation clientInvoke = createValidationInvocation(getFhirContext(), "$validate", theContents, theExtraParams); + MethodOutcome resp = invokeClient(getFhirContext(), binding, clientInvoke, null, null, false, null, null, null, null, null); + return resp.getOperationOutcome(); + } + +} diff --git a/matchbox-server/src/test/resources/application-test.yaml b/matchbox-server/src/test/resources/application-test.yaml new file mode 100644 index 00000000000..2d0a45478e9 --- /dev/null +++ b/matchbox-server/src/test/resources/application-test.yaml @@ -0,0 +1,31 @@ +server: + port: 8081 + servlet: + context-path: /matchboxv3 +hapi: + fhir: + implementationguides: + fhir_r4_core: + name: hl7.fhir.r4.core + version: 4.0.1 + url: classpath:/hl7.fhir.r4.core.tgz + fhir_terminology: + name: hl7.terminology + version: 5.4.0 + url: classpath:/hl7.terminology#5.4.0.tgz + fhir_extensions: + name: hl7.fhir.uv.extensions.r4 + version: 1.0.0 + url: classpath:/hl7.fhir.uv.extensions.r4#1.0.0.tgz + matchbox_health_test_ig_r4: + name: matchbox.health.test.ig.r4 + version: 0.1.0 + url: classpath:/matchbox.health.test.ig.r4-0.1.0.tgz +matchbox: + fhir: + context: + txServer: http://localhost:${server.port}/matchboxv3/fhir + suppressWarnInfo: + hl7.fhir.r4.core#4.0.1: + - "Constraint failed: dom-6:" + - "regex:Entry '(.+)' isn't reachable by traversing forwards from the Composition" \ No newline at end of file diff --git a/matchbox-server/src/test/resources/ehs-419.json b/matchbox-server/src/test/resources/ehs-419.json new file mode 100644 index 00000000000..688392c5108 --- /dev/null +++ b/matchbox-server/src/test/resources/ehs-419.json @@ -0,0 +1,9 @@ +{ + "resourceType": "Patient", + "id": "example", + "language": "de", + "text": { + "status": "generated", + "div": "
42\n
" + } +} \ No newline at end of file diff --git a/matchbox-server/src/test/resources/ehs-431.json b/matchbox-server/src/test/resources/ehs-431.json new file mode 100644 index 00000000000..cec8419f8f7 --- /dev/null +++ b/matchbox-server/src/test/resources/ehs-431.json @@ -0,0 +1,509 @@ +[{ + + "resourceType": "Bundle", + + "id": "171", + + "meta": { + + "versionId": "1", + + "lastUpdated": "2020-09-24T10:24:34.481+02:00", + + "profile": [ + + "http://fhir.ch/ig/ch-emed/StructureDefinition/ch-emed-document-pharmaceuticaladvice" + + ] + + }, + + "identifier": { + + "system": "urn:ietf:rfc:3986", + + "value": "urn:uuid:552647_TST" + + }, + + "type": "document", + + "timestamp": "2020-09-24T10:12:34.000+02:00", + + "entry": [ + + { + + "fullUrl": "http://cistec.com/r4/\tComposition/35740112-9f9b-4258-8943-f682abac5fef_PA_AGL4703276", + + "resource": { + + "resourceType": "Composition", + + "id": "35740112-9f9b-4258-8943-f682abac5fef_PA_AGL4703276", + + "language": "de-CH", + + "text": { + + "status": "generated", + + "div": "
$composition = org.hl7.fhir.r4.model.Composition@46eab393\r\n

Generated Narrative

\r\n

id: $composition.getId()

\r\n

language: $composition.getLanguage()

\r\n

identifier: id: $composition.getIdentifier().getId()

\r\n

status: $composition.getStatus().toCode()

\r\n

type: Medication summary

\r\n

date:

\r\n

author:

\r\n
    \r\n
\r\n

title: Medikationsplan

\r\n

confidentiality: $composition.getConfidentiality().getDisplay()

\r\n

custodian: See $composition.getCustodian().getId()

" + + }, + + "extension": [ + + { + + "url": "http://fhir.ch/ig/ch-core/StructureDefinition/ch-ext-epr-setid", + + "valueIdentifier": { + + "system": "urn:ietf:rfc:3986", + + "value": "urn:uuid:35740112-9f9b-4258-8943-f682abac5fef_PA_AGL4703276" + + } + + }, + + { + + "url": "http://fhir.ch/ig/ch-core/StructureDefinition/ch-ext-epr-versionnumber", + + "valueUnsignedInt": 2 + + }, + + { + + "url": "http://fhir.ch/ig/ch-core/StructureDefinition/ch-ext-epr-informationrecipient", + + "valueReference": { + + "reference": "Patient/TU681" + + } + + } + + ], + + "identifier": { + + "system": "urn:ietf:rfc:3986", + + "value": "urn:uuid:35740112-9f9b-4258-8943-f682abac5fef_PA_AGL4703276" + + }, + + "status": "final", + + "type": { + + "coding": [ + + { + + "system": "http://snomed.info/sct", + + "code": "419891008", + + "display": "Record artifact (record artifact)" + + }, + + { + + "system": "http://loinc.org", + + "code": "61356-2", + + "display": "Medication pharmaceutical advice.extended" + + } + + ] + + }, + + "subject": { + + "reference": "Patient/TU681" + + }, + + "date": "2020-09-24T10:12:34+02:00", + + "author": [ + + { + + "extension": [ + + { + + "url": "http://fhir.ch/ig/ch-core/StructureDefinition/ch-ext-epr-time", + + "valueDateTime": "2020-09-24T10:12:34+02:00" + + } + + ], + + "reference": "Practitioner/CISEESCH" + + }, + + { + + "reference": "Organization/COM" + + } + + ], + + "title": "Kommentar zur Medikation", + + "confidentiality": "N", + + "_confidentiality": { + + "extension": [ + + { + + "url": "http://fhir.ch/ig/ch-core/StructureDefinition/ch-ext-epr-confidentialitycode", + + "valueCodeableConcept": { + + "coding": [ + + { + + "system": "http://snomed.info/sct", + + "code": "17621005", + + "display": "Normally accessible" + + } + + ] + + } + + } + + ] + + }, + + "custodian": { + + "reference": "Organization/COM" + + }, + + "section": [ + + { + + "extension": [ + + { + + "url": "http://fhir.ch/ig/ch-core/StructureDefinition/ch-ext-epr-sectionid", + + "valueIdentifier": { + + "system": "urn:ietf:rfc:3986", + + "value": "urn:uuid:8ed02d0a-2971-11e6-b67b-9e71128cae77" + + } + + } + + ], + + "title": "Hinweise zur Medikation", + + "code": { + + "coding": [ + + { + + "system": "http://loinc.org", + + "code": "61357-0", + + "display": "Medication pharmaceutical advice.brief" + + } + + ] + + }, + + "text": { + + "status": "generated", + + "div": "
TODO add text
" + + }, + + "entry": [ + + { + + "reference": "Observation/AGL4703276" + + } + + ] + + } + + ] + + } + + }, + + { + + "fullUrl": "http://cistec.com/r4/\tPatient/TU681", + + "resource": { + + "resourceType": "Patient", + + "id": "TU681", + + "text": { + + "status": "generated", + + "div": "
Gebhard August ANTONYOVA
IdentifierTU681
Date of birth06 September 1969
" + + }, + + "identifier": [ + + { + + "type": { + + "coding": [ + + { + + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + + "code": "MR" + + } + + ] + + }, + + "system": "http://cistec.com/DEV_COM/Patient", + + "value": "TU681" + + } + + ], + + "name": [ + + { + + "family": "Antonyova", + + "given": [ + + "Gebhard August" + + ] + + } + + ], + + "gender": "male", + + "birthDate": "1969-09-06" + + } + + }, + + { + + "fullUrl": "http://cistec.com/r4/\tPractitioner/CISEESCH", + + "resource": { + + "resourceType": "Practitioner", + + "id": "CISEESCH", + + "identifier": [ + + { + + "system": "http://cistec.com/DEV_COM/Practitioner", + + "value": "CISEESCH" + + }, + + { + + "system": "urn:oid:2.51.1.3", + + "value": "123456789" + + } + + ], + + "name": [ + + { + + "family": "Eschmann", + + "given": [ + + "Emmanuel" + + ] + + } + + ], + + "gender": "unknown" + + } + + }, + + { + + "fullUrl": "http://cistec.com/r4/\tOrganization/COM", + + "resource": { + + "resourceType": "Organization", + + "id": "COM", + + "identifier": [ + + { + + "system": "http://cistec.com/DEV_COM/Organization", + + "value": "2.16.756.5.30.1.163.1.1" + + } + + ], + + "type": [ + + { + + "coding": [ + + { + + "system": "http://snomed.info/sct", + + "code": "22232009", + + "display": "Hospital" + + } + + ] + + } + + ], + + "name": "Cistec COM Spital" + + } + + }, + + { + + "fullUrl": "http://cistec.com/r4/\tObservation/AGL4703276", + + "resource": { + + "resourceType": "Observation", + + "id": "AGL4703276", + + "text": { + + "status": "generated", + + "div": "
TODO add text
" + + }, + + "status": "final", + + "code": { + + "coding": [ + + { + + "system": "urn:oid:1.3.6.1.4.1.19376.1.9.2.1", + + "code": "CANCEL", + + "display": "CANCEL" + + } + + ] + + }, + + "subject": { + + "reference": "Patient/TU681" + + }, + + "note": [ + + { + + "text": "Tabl p.o." + + } + + ] + + } + + } + + ] + +}] diff --git a/matchbox-server/src/test/resources/logback-test.xml b/matchbox-server/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..71985e14b1e --- /dev/null +++ b/matchbox-server/src/test/resources/logback-test.xml @@ -0,0 +1,12 @@ + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n + + + + + + + \ No newline at end of file diff --git a/matchbox-server/src/test/resources/matchbox.health.test.ig.r4-0.1.0.tgz b/matchbox-server/src/test/resources/matchbox.health.test.ig.r4-0.1.0.tgz new file mode 100644 index 00000000000..9b1fb897c3f Binary files /dev/null and b/matchbox-server/src/test/resources/matchbox.health.test.ig.r4-0.1.0.tgz differ diff --git a/matchbox-server/with-cda/application.yaml b/matchbox-server/with-cda/application.yaml index 54765b76a6a..fa25ec48549 100644 --- a/matchbox-server/with-cda/application.yaml +++ b/matchbox-server/with-cda/application.yaml @@ -26,5 +26,4 @@ matchbox: fhir: context: fhirVersion: 4.0.1 - txServer: http://tx.fhir.org - onlyOneEngine: true + txServer: http://tx.fhir.org \ No newline at end of file diff --git a/pom.xml b/pom.xml index a2245d6b643..73f00c73173 100644 --- a/pom.xml +++ b/pom.xml @@ -520,6 +520,10 @@ src/main/resources false
+ + src/test/resources + false + ${project.build.directory}/generated-sources/properties false