diff --git a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/i18n/I18nBaseTest.java b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/i18n/I18nBaseTest.java index 1edc79f80..f1b7e2b2c 100644 --- a/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/i18n/I18nBaseTest.java +++ b/org.hl7.fhir.utilities/src/test/java/org/hl7/fhir/utilities/i18n/I18nBaseTest.java @@ -164,4 +164,17 @@ class I18nBaseTest { assertNull(enLocale.getRootKeyFromPlural(rootKey + "_many")); } + @Test + public void testMessagesChangeWhenLocaleDoes() { + I18nTestClass i18nInstance = new I18nTestClass(); + i18nInstance.setLocale(Locale.forLanguageTag("de")); + + String deMessage = i18nInstance.formatMessage(I18nConstants.ERROR_PARSING_JSON_, "test"); + assertEquals("Fehler beim Parsen von JSON: test", deMessage); + + i18nInstance.setLocale(Locale.forLanguageTag("en")); + String enMessage = i18nInstance.formatMessage(I18nConstants.ERROR_PARSING_JSON_, "test"); + assertEquals("Error parsing JSON: test", enMessage); + + } } \ No newline at end of file diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java index 925673c4a..e0a1a9edc 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/IgLoader.java @@ -316,7 +316,7 @@ public class IgLoader implements IValidationEngineLoader { public void scanForIgVersion(String src, boolean recursive, - VersionSourceInformation versions) throws Exception { + VersionSourceInformation versions) throws IOException { Map source = loadIgSourceForVersion(src, recursive, true, versions); if (source != null) { if (source.containsKey("version.info")) { diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java index 1d9ab26d6..2e8631384 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/ValidatorCli.java @@ -105,7 +105,7 @@ public class ValidatorCli { public static final String JAVA_DISABLED_PROXY_SCHEMES = "jdk.http.auth.proxying.disabledSchemes"; public static final String JAVA_USE_SYSTEM_PROXIES = "java.net.useSystemProxies"; - private static ValidationService validationService = new ValidationService(); + private final static ValidationService validationService = new ValidationService(); protected ValidationService myValidationService; @@ -408,8 +408,4 @@ public class ValidatorCli { return validationEngine; } - protected void validateScan(CliContext cliContext, ValidationEngine validator) throws Exception { - Scanner validationScanner = new Scanner(validator.getContext(), validator.getValidator(null), validator.getIgLoader(), validator.getFhirPathEngine()); - validationScanner.validateScan(cliContext.getOutput(), cliContext.getSources()); - } } \ 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 e47235246..f719f029b 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 @@ -24,6 +24,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; */ public class CliContext { + @JsonProperty("baseEngine") + private String baseEngine = null; @JsonProperty("doNative") private boolean doNative = false; @JsonProperty("hintAboutNonMustSupport") @@ -160,7 +162,17 @@ public class CliContext { @JsonProperty("bestPracticeLevel") private BestPracticeWarningLevel bestPracticeLevel = BestPracticeWarningLevel.Warning; - + + @JsonProperty("baseEngine") + public String getBaseEngine() { + return baseEngine; + } + + @JsonProperty("baseEngine") + public CliContext setBaseEngine(String baseEngine) { + this.baseEngine = baseEngine; + return this; + } @JsonProperty("map") public String getMap() { @@ -771,7 +783,8 @@ public class CliContext { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CliContext that = (CliContext) o; - return doNative == that.doNative && + return Objects.equals(baseEngine, that.baseEngine) && + doNative == that.doNative && hintAboutNonMustSupport == that.hintAboutNonMustSupport && recursive == that.recursive && doDebug == that.doDebug && @@ -824,7 +837,7 @@ public class CliContext { @Override public int hashCode() { - return Objects.hash(doNative, extensions, hintAboutNonMustSupport, recursive, doDebug, assumeValidRestReferences, canDoNative, noInternalCaching, + return Objects.hash(baseEngine, doNative, extensions, hintAboutNonMustSupport, recursive, doDebug, assumeValidRestReferences, canDoNative, noInternalCaching, noExtensibleBindingMessages, noInvariants, displayWarnings, wantInvariantsInMessages, map, output, outputSuffix, htmlOutput, txServer, sv, txLog, txCache, mapLog, lang, srcLang, tgtLang, fhirpath, snomedCT, targetVer, igs, questionnaireMode, level, profiles, sources, inputs, mode, locale, locations, crumbTrails, forPublication, showTimes, allowExampleUrls, outputStyle, jurisdiction, noUnicodeBiDiControlChars, watchMode, watchScanDelay, watchSettleTime, bestPracticeLevel, htmlInMarkdownCheck, allowDoubleQuotesInFHIRPath, checkIPSCodes); @@ -833,7 +846,8 @@ public class CliContext { @Override public String toString() { return "CliContext{" + - "doNative=" + doNative + + "baseEngine=" + baseEngine + + ", doNative=" + doNative + ", extensions=" + extensions + ", hintAboutNonMustSupport=" + hintAboutNonMustSupport + ", recursive=" + recursive + diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/PassiveExpiringSessionCache.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/PassiveExpiringSessionCache.java index b3a9a18b6..3293a33e6 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/PassiveExpiringSessionCache.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/PassiveExpiringSessionCache.java @@ -117,6 +117,11 @@ public class PassiveExpiringSessionCache implements SessionCache { return cachedSessions.keySet(); } + @Override + public void cleanUp() { + removeExpiredSessions(); + } + /** * Session ids generated internally are UUID {@link String}. * @return A new {@link String} session id. diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/SessionCache.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/SessionCache.java index 11b5afd0d..40572f3ad 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/SessionCache.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/SessionCache.java @@ -23,9 +23,6 @@ public interface SessionCache { */ String cacheSession(String sessionId, ValidationEngine validationEngine); - - - /** * Checks if the passed in {@link String} id exists in the set of stored session id. * @param sessionId The {@link String} id to search for. @@ -45,5 +42,10 @@ public interface SessionCache { * @return {@link Set} of session ids. */ Set getSessionIds(); - + + /** + * Performs any pending maintenance operations needed by the cache. + * */ + public void cleanUp(); + } \ No newline at end of file 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 ff7954431..2fb45f0e6 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 @@ -2,7 +2,6 @@ package org.hl7.fhir.validation.cli.services; import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; @@ -11,6 +10,7 @@ import java.lang.management.MemoryMXBean; import java.net.URISyntaxException; import java.text.SimpleDateFormat; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nonnull; @@ -75,25 +75,41 @@ public class ValidationService { private final SessionCache sessionCache; private String runDate; + private final Map baseEngines = new ConcurrentHashMap<>(); + + public void putBaseEngine(String key, CliContext cliContext) throws IOException, URISyntaxException { + if (cliContext.getSv() == null) { + throw new IllegalArgumentException("Cannot create a base engine without an explicit version"); + } + String definitions = VersionUtilities.packageForVersion(cliContext.getSv()) + "#" + VersionUtilities.getCurrentVersion(cliContext.getSv()); + + ValidationEngine baseEngine = buildValidationEngine(cliContext, definitions, new TimeTracker()); + baseEngines.put(key, baseEngine); + } + + public ValidationEngine getBaseEngine(String key) { + return baseEngines.get(key); + } + + public Set getBaseEngineKeys() { return baseEngines.keySet(); } + + public boolean hasBaseEngineForKey(String key) { return baseEngines.containsKey(key); } + public ValidationService() { sessionCache = new PassiveExpiringSessionCache(); runDate = new SimpleDateFormat("hh:mm:ss", new Locale("en", "US")).format(new Date()); } + + public ValidationService(SessionCache cache) { this.sessionCache = cache; } public ValidationResponse validateSources(ValidationRequest request) throws Exception { - if (request.getCliContext().getSv() == null) { - String sv = determineVersion(request.getCliContext(), request.sessionId); - request.getCliContext().setSv(sv); - } - - String definitions = VersionUtilities.packageForVersion(request.getCliContext().getSv()) + "#" + VersionUtilities.getCurrentVersion(request.getCliContext().getSv()); TimeTracker timeTracker = new TimeTracker(); - String sessionId = initializeValidator(request.getCliContext(), definitions, timeTracker, request.sessionId); + String sessionId = initializeValidator(request.getCliContext(), null, timeTracker, request.sessionId); ValidationEngine validator = sessionCache.fetchSessionValidatorEngine(sessionId); if (request.getCliContext().getProfiles().size() > 0) { @@ -180,7 +196,7 @@ public class ValidationService { return outcome; } - public VersionSourceInformation scanForVersions(CliContext cliContext) throws Exception { + public VersionSourceInformation scanForVersions(CliContext cliContext) throws IOException { VersionSourceInformation versions = new VersionSourceInformation(); IgLoader igLoader = new IgLoader( new FilesystemPackageCacheManager.Builder().build(), @@ -473,16 +489,42 @@ public class ValidationService { if (sessionId != null) { System.out.println("No such cached session exists for session id " + sessionId + ", re-instantiating validator."); } - System.out.println("Building new validator engine from CliContext"); - ValidationEngine validator = buildValidationEngine(cliContext, definitions, tt); - sessionId = sessionCache.cacheSession(validator); + sessionCache.cleanUp(); + if (cliContext.getSv() == null) { + String sv = determineVersion(cliContext); + cliContext.setSv(sv); + } + final String engineDefinitions = definitions != null ? definitions : VersionUtilities.packageForVersion(cliContext.getSv()) + "#" + VersionUtilities.getCurrentVersion(cliContext.getSv()); + + ValidationEngine validationEngine = getValidationEngineFromCliContext(cliContext, engineDefinitions, tt); + sessionId = sessionCache.cacheSession(validationEngine); System.out.println("Cached new session. Cache size = " + sessionCache.getSessionIds().size()); + } else { System.out.println("Cached session exists for session id " + sessionId + ", returning stored validator session id. Cache size = " + sessionCache.getSessionIds().size()); } return sessionId; } + private ValidationEngine getValidationEngineFromCliContext(CliContext cliContext, String definitions, TimeTracker tt) throws Exception { + ValidationEngine validationEngine; + if (cliContext.getBaseEngine() != null && hasBaseEngineForKey(cliContext.getBaseEngine())) { + validationEngine = new ValidationEngine(getBaseEngine(cliContext.getBaseEngine())); + /* As a service, it wouldn't be efficient to have a base validation engine + * for every language. So we just use the baseEngine and set the language + * manually afterward. + */ + validationEngine.setLanguage(cliContext.getLang()); + validationEngine.setLocale(cliContext.getLocale()); + } else { + if (definitions == null) { + throw new IllegalArgumentException("Cannot create a validator engine (definitions == null)"); + } + validationEngine = buildValidationEngine(cliContext, definitions, tt); + } + return validationEngine; + } + protected ValidationEngine.ValidationEngineBuilder getValidationEngineBuilder() { return new ValidationEngine.ValidationEngineBuilder(); } @@ -564,12 +606,7 @@ public class ValidationService { System.out.println(" Package Summary: "+ validationEngine.getContext().loadedPackageSummary()); } - - public String determineVersion(CliContext cliContext) throws Exception { - return determineVersion(cliContext, null); - } - - public String determineVersion(CliContext cliContext, String sessionId) throws Exception { + public String determineVersion(CliContext cliContext) throws IOException { if (cliContext.getMode() != EngineMode.VALIDATION && cliContext.getMode() != EngineMode.INSTALL) { return "5.0"; } @@ -588,7 +625,7 @@ public class ValidationService { System.out.println("-> use version " + versions.version()); return versions.version(); } - throw new Exception("-> Multiple versions found. Specify a particular version using the -version parameter"); + throw new IllegalArgumentException("-> Multiple versions found. Specify a particular version using the -version parameter"); } public void generateSpreadsheet(CliContext cliContext, ValidationEngine validator) throws Exception { diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/ValidationServiceTest.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/ValidationServiceTests.java similarity index 85% rename from org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/ValidationServiceTest.java rename to org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/ValidationServiceTests.java index 5346d78e8..1a090cf84 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/ValidationServiceTest.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/ValidationServiceTests.java @@ -44,7 +44,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; -class ValidationServiceTest { +class ValidationServiceTests { final String DUMMY_SOURCE = "dummySource"; final String DUMMY_SOURCE1 = "dummySource1"; @@ -54,34 +54,74 @@ class ValidationServiceTest { final String DUMMY_SV = "1.2.3"; + @DisplayName("Test validation session persists in session cache") @Test - void validateSources() throws Exception { + void validationSessionTest() throws Exception { TestingUtilities.injectCorePackageLoader(); SessionCache sessionCache = Mockito.spy(new PassiveExpiringSessionCache()); - ValidationService myService = new ValidationService(sessionCache); + ValidationService myService = Mockito.spy(new ValidationService(sessionCache)); - String resource = IOUtils.toString(getFileFromResourceAsStream("detected_issues.json"), StandardCharsets.UTF_8); - List filesToValidate = new ArrayList<>(); - filesToValidate.add(new FileInfo().setFileName("test_resource.json").setFileContent(resource).setFileType(Manager.FhirFormat.JSON.getExtension())); + List filesToValidate = getFilesToValidate(); ValidationRequest request = new ValidationRequest().setCliContext(new CliContext().setTxServer(FhirSettings.getTxFhirDevelopment()).setTxCache(getTerminologyCacheDirectory("validationService"))).setFilesToValidate(filesToValidate); // Validation run 1...nothing cached yet myService.validateSources(request); verify(sessionCache, Mockito.times(1)).cacheSession(ArgumentMatchers.any(ValidationEngine.class)); - + verify(sessionCache, Mockito.times(1)).cleanUp(); + verify(myService, Mockito.times(1)).buildValidationEngine(any(), any(), any()); Set sessionIds = sessionCache.getSessionIds(); if (sessionIds.stream().findFirst().isPresent()) { // Verify that after 1 run there is only one entry within the cache Assertions.assertEquals(1, sessionIds.size()); - myService.validateSources(request); - // Verify that the cache has been called on once with the id created in the first run - verify(sessionCache, Mockito.times(1)).fetchSessionValidatorEngine(sessionIds.stream().findFirst().get()); + myService.validateSources(request.setSessionId(sessionIds.stream().findFirst().get())); + // Verify that the cache has been called on twice with the id created in the first run + verify(sessionCache, Mockito.times(2)).fetchSessionValidatorEngine(sessionIds.stream().findFirst().get()); + verify(sessionCache, Mockito.times(1)).cleanUp(); + verify(myService, Mockito.times(1)).buildValidationEngine(any(), any(), any()); } else { // If no sessions exist within the cache after a run, we auto-fail. fail(); } } + @DisplayName("Test validation session will inherit a base validation engine") + @Test + void validationSessionBaseEngineTest() throws Exception { + TestingUtilities.injectCorePackageLoader(); + + ValidationService myService = Mockito.spy(new ValidationService()); + + CliContext baseContext = new CliContext().setBaseEngine("myDummyKey").setSv("4.0.1").setTxServer(FhirSettings.getTxFhirDevelopment()).setTxCache(getTerminologyCacheDirectory("validationService")); + myService.putBaseEngine("myDummyKey", baseContext); + verify(myService, Mockito.times(1)).buildValidationEngine(any(), any(), any()); + + { + final List filesToValidate = getFilesToValidate(); + final ValidationRequest request = new ValidationRequest().setCliContext(new CliContext().setSv("4.0.1")).setFilesToValidate(filesToValidate); + myService.validateSources(request); + + verify(myService, Mockito.times(0)).getBaseEngine("myDummyKey"); + verify(myService, Mockito.times(2)).buildValidationEngine(any(), any(), any()); + } + + { + final List filesToValidate = getFilesToValidate(); + final ValidationRequest request = new ValidationRequest().setCliContext(new CliContext().setBaseEngine("myDummyKey")).setFilesToValidate(filesToValidate); + myService.validateSources(request); + + verify(myService, Mockito.times(1)).getBaseEngine("myDummyKey"); + verify(myService, Mockito.times(2)).buildValidationEngine(any(), any(), any()); + } + } + + private List getFilesToValidate() throws IOException { + List filesToValidate = new ArrayList<>(); + String resource = IOUtils.toString(getFileFromResourceAsStream("detected_issues.json"), StandardCharsets.UTF_8); + + filesToValidate.add(new FileInfo().setFileName("test_resource.json").setFileContent(resource).setFileType(Manager.FhirFormat.JSON.getExtension())); + return filesToValidate; + } + private InputStream getFileFromResourceAsStream(String fileName) { // The class loader that loaded the class ClassLoader classLoader = getClass().getClassLoader(); diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java index f62178dd4..55510b284 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationEngineTests.java @@ -1,5 +1,6 @@ package org.hl7.fhir.validation.tests; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; @@ -18,6 +19,7 @@ import org.hl7.fhir.validation.IgLoader; import org.hl7.fhir.validation.ValidationEngine; import org.hl7.fhir.validation.tests.utilities.TestUtilities; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public class ValidationEngineTests { @@ -27,6 +29,88 @@ public class ValidationEngineTests { public static boolean inbuild; + @Test + @DisplayName("A ValidationEngine copied from another validation engine shouldn't interfere with the original during validations") + void validateWithParallelCopiedEngine() throws Exception { + + final String INPUT_1 = "patient-duplicate.json"; + final String INPUT_2 = "patient-lang1.json"; + final String INPUT_3 = "patient-id-bad-1.json"; + + final String[] ISSUE_CODES_1 = { "invalid" }; + final String[] ISSUE_CODES_2 = {"business-rule"}; + final String[] ISSUE_CODES_3 = {"invalid", "invariant"}; + + ValidationEngine originalEngine = TestUtilities.getValidationEngine("hl7.fhir.r4.core#4.0.1", DEF_TX, FhirPublication.R4, "4.0.1"); + + final ValidationEngine[] validationEngines = new ValidationEngine[10]; + validationEngines[0] = originalEngine; + + final OperationOutcome[] outcomes = new OperationOutcome[validationEngines.length]; + + for (int i = 1; i < validationEngines.length; i++) { + validationEngines[i] = new ValidationEngine(originalEngine); + } + + final String[] testInputs = { + INPUT_1, + INPUT_1, + INPUT_2, + INPUT_3, + INPUT_1, + INPUT_2, + INPUT_3, + INPUT_1, + INPUT_2, + INPUT_3 + }; + // Pick 3 validation cases + final String[][] testCodes = { + ISSUE_CODES_1, + ISSUE_CODES_1, + ISSUE_CODES_2, + ISSUE_CODES_3, + ISSUE_CODES_1, + ISSUE_CODES_2, + ISSUE_CODES_3, + ISSUE_CODES_1, + ISSUE_CODES_2, + ISSUE_CODES_3 + }; + + + List threads = new ArrayList<>(); + for (int i = 0; i < validationEngines.length; i++) { + final int index = i; + Thread t = new Thread(() -> { + try { + final String testInput = testInputs[index]; + outcomes[index] = validationEngines[index].validate(FhirFormat.JSON, TestingUtilities.loadTestResourceStream("validator", testInput), null); + } catch (Exception e) { + e.printStackTrace(); + System.err.println("Thread " + index + " failed"); + } + }); + t.start(); + threads.add(t); + } + threads.forEach(t -> { + try { + t.join(); + } catch (InterruptedException e) { + + } + }); + + for (int i = 0; i < outcomes.length; i++) { + assertEquals(testCodes[i].length, outcomes[i].getIssue().size()); + for (int j = 0; j < outcomes[i].getIssue().size(); j++) { + System.out.print(i + " " + j); + assertEquals(testCodes[i][j], outcomes[i].getIssue().get(j).getCode().toCode()); + } + } + } + @Test public void test401Xml() throws Exception { if (!TestUtilities.silent)