Merge pull request #1640 from hapifhir/do-20240122-base-engine

Maintain a map of pre-built ValidationEngine instances
This commit is contained in:
Grahame Grieve 2024-07-11 07:22:21 +08:00 committed by GitHub
commit 53ab71f118
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 234 additions and 43 deletions

View File

@ -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);
}
}

View File

@ -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<String, ByteProvider> source = loadIgSourceForVersion(src, recursive, true, versions);
if (source != null) {
if (source.containsKey("version.info")) {

View File

@ -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());
}
}

View File

@ -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 +

View File

@ -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.

View File

@ -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<String> getSessionIds();
/**
* Performs any pending maintenance operations needed by the cache.
* */
public void cleanUp();
}

View File

@ -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<String, ValidationEngine> 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<String> 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 {

View File

@ -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<FileInfo> filesToValidate = new ArrayList<>();
filesToValidate.add(new FileInfo().setFileName("test_resource.json").setFileContent(resource).setFileType(Manager.FhirFormat.JSON.getExtension()));
List<FileInfo> 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<String> 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<FileInfo> 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<FileInfo> 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<FileInfo> getFilesToValidate() throws IOException {
List<FileInfo> 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();

View File

@ -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<Thread> 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)