diff --git a/core/pom.xml b/core/pom.xml index 5c92ccf4..2e3ecab0 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -3,7 +3,6 @@ 4.0.0 - org.opencds.cqf.cql.ls cql-ls-core jar CQL Language Server Core @@ -21,6 +20,16 @@ org.opencds.cqf.fhir cqf-fhir-cr + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlin + kotlin-stdlib-common + + \ No newline at end of file diff --git a/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.java b/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.java new file mode 100644 index 00000000..19f47637 --- /dev/null +++ b/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.java @@ -0,0 +1,36 @@ +package org.opencds.cqf.cql.ls.core.utility; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import kotlinx.io.Buffer; +import kotlinx.io.Source; + +public class Converters { + + public static String inputStreamToString(InputStream inputStream) throws IOException { + StringBuilder resultStringBuilder = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (!resultStringBuilder.isEmpty()) resultStringBuilder.append("\n"); // Append newline if needed + resultStringBuilder.append(line); + } + } + return resultStringBuilder.toString(); + } + + public static Source stringToSource(String text) { + Buffer buffer = new Buffer(); + // Write the string to the buffer using a specific character encoding + buffer.write(text.getBytes(), 0, text.length()); + // Return the buffer as a Source + return buffer; + } + + public static Source inputStreamToSource(InputStream inputStream) throws IOException { + return stringToSource(inputStreamToString(inputStream)); + } +} diff --git a/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.java b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.java new file mode 100644 index 00000000..0ec444a7 --- /dev/null +++ b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.java @@ -0,0 +1,79 @@ +package org.opencds.cqf.cql.ls.core.utility; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +public class ConvertersTest { + + @Test + void should_returnInstance_when_creatingConverter() { + var converter = new Converters(); + assertNotNull(converter); + } + + @Test + void should_returnString_when_inputStreamExists() { + var expected = "The quick brown fox jumps over the lazy dog"; + try { + var actual = + Converters.inputStreamToString(new ByteArrayInputStream(expected.getBytes(StandardCharsets.UTF_8))); + assertEquals(expected, actual); + } catch (IOException e) { + fail("Unexpected exception thrown. {}", e); + } + } + + @Test + void should_returnStringWithLineBreaks_when_inputStreamHasLineBreaksExists() { + var expected = + """ + the first day in spring – + a wind from the ocean + but no ocean in sight"""; + try { + var actual = + Converters.inputStreamToString(new ByteArrayInputStream(expected.getBytes(StandardCharsets.UTF_8))); + assertEquals(expected, actual); + } catch (IOException e) { + fail("Unexpected exception thrown. {}", e); + } + } + + @Test + void should_throwIOException_when_inputStreamToStringHasAnIOError() throws IOException { + InputStream inputStream = mock(InputStream.class); + when(inputStream.read()).thenThrow(new IOException("Simulated failure")); + assertThrows(IOException.class, () -> Converters.inputStreamToString(inputStream)); + } + + @Test + void should_returnSource_when_stringExists() { + var expected = Converters.stringToSource("The quick brown fox jumps over the lazy dog"); + assertNotNull(expected); + } + + @Test + void should_returnSource_when_inputStreamExists() { + try { + var expected = Converters.inputStreamToSource(new ByteArrayInputStream( + "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8))); + assertNotNull(expected); + } catch (IOException e) { + fail("Unexpected exception thrown. {}", e); + } + } + + @Test + void should_throwIOException_when_inputStreamToSourceCalledWithNull() throws IOException { + InputStream inputStream = mock(InputStream.class); + when(inputStream.read()).thenThrow(new IOException("Simulated failure")); + assertThrows(IOException.class, () -> Converters.inputStreamToSource(inputStream)); + } +} diff --git a/debug/server/pom.xml b/debug/server/pom.xml index 46c9729a..8b55c5f0 100644 --- a/debug/server/pom.xml +++ b/debug/server/pom.xml @@ -20,7 +20,17 @@ org.opencds.cqf.cql.ls cql-ls-core - 4.0.0-SNAPSHOT + ${project.version} + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlin + kotlin-stdlib-common + + org.eclipse.lsp4j diff --git a/debug/service/pom.xml b/debug/service/pom.xml index c1aedd26..147f9135 100644 --- a/debug/service/pom.xml +++ b/debug/service/pom.xml @@ -20,17 +20,15 @@ org.opencds.cqf.cql.debug cql-debug-server - 4.0.0-SNAPSHOT + ${project.version} org.springframework.boot spring-boot - ${spring.boot.version} org.springframework.boot spring-boot-starter-logging - ${spring.boot.version} diff --git a/ls/server/pom.xml b/ls/server/pom.xml index 26630c8e..95c3dcc6 100644 --- a/ls/server/pom.xml +++ b/ls/server/pom.xml @@ -3,7 +3,6 @@ 4.0.0 - org.opencds.cqf.cql.ls cql-ls-server jar CQL Language Server - Server Component @@ -21,8 +20,9 @@ ca.uhn.hapi.fhir hapi-fhir-caching-caffeine - 7.0.0 + 8.6.0 + org.springframework.boot spring-boot @@ -31,18 +31,37 @@ org.opencds.cqf.cql.ls cql-ls-core - 4.0.0-SNAPSHOT + ${project.version} org.opencds.cqf.fhir cqf-fhir-cr + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlin + kotlin-stdlib-common + + - org.opencds.cqf.fhir - cqf-fhir-jackson - pom + org.cqframework + cql-formatter + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlin + kotlin-stdlib-common + + @@ -58,26 +77,11 @@ com.fasterxml.jackson.core jackson-databind - - - com.fasterxml.jackson.core - jackson-annotations - - - - info.cqframework - cql-to-elm - - - info.cqframework - model - - info.cqframework - cql-formatter + com.fasterxml.jackson.core + jackson-annotations @@ -95,6 +99,21 @@ info.picocli picocli + + + + jakarta.annotation + jakarta.annotation-api + 2.1.1 + + + + + org.opencds.cqf.fhir + cqf-fhir-test + test + + @@ -104,9 +123,6 @@ maven-compiler-plugin 3.10.0 - 11 - 11 - 11 true true true diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.java index 4f0015f8..30fced74 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.java @@ -1,5 +1,7 @@ package org.opencds.cqf.cql.ls.server.command; +import static kotlinx.io.files.PathsKt.Path; + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.repository.IRepository; @@ -11,7 +13,6 @@ import java.util.concurrent.Callable; import org.apache.commons.lang3.tuple.Pair; import org.cqframework.cql.cql2elm.CqlTranslatorOptions; -import org.cqframework.cql.cql2elm.CqlTranslatorOptionsMapper; import org.cqframework.cql.cql2elm.DefaultLibrarySourceProvider; import org.cqframework.cql.cql2elm.DefaultModelInfoProvider; import org.cqframework.fhir.npm.NpmProcessor; @@ -20,10 +21,11 @@ import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r5.context.IWorkerContext.ILoggingService; +import org.hl7.fhir.r5.context.ILoggingService; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.execution.ExpressionResult; import org.opencds.cqf.cql.ls.core.utility.Uris; +import org.opencds.cqf.cql.ls.server.repository.ig.standard.IgStandardRepository; import org.opencds.cqf.fhir.cql.CqlOptions; import org.opencds.cqf.fhir.cql.Engines; import org.opencds.cqf.fhir.cql.EvaluationSettings; @@ -37,7 +39,6 @@ import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_MEMBERSHIP_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_PRE_EXPANSION_MODE; import org.opencds.cqf.fhir.utility.repository.ProxyRepository; -import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; import org.slf4j.LoggerFactory; import picocli.CommandLine; import picocli.CommandLine.ArgGroup; @@ -150,16 +151,12 @@ public boolean isDebugLogging() { } private String toVersionNumber(FhirVersionEnum fhirVersion) { - switch (fhirVersion) { - case R4: - return "4.0.1"; - case R5: - return "5.0.0-ballot"; - case DSTU3: - return "3.0.2"; - default: - throw new IllegalArgumentException(String.format("Unsupported FHIR version %s", fhirVersion)); - } + return switch (fhirVersion) { + case R4 -> "4.0.1"; + case R5 -> "5.0.0-ballot"; + case DSTU3 -> "3.0.2"; + default -> throw new IllegalArgumentException(String.format("Unsupported FHIR version %s", fhirVersion)); + }; } @CommandLine.ParentCommand @@ -188,8 +185,8 @@ public Integer call() throws Exception { CqlOptions cqlOptions = CqlOptions.defaultOptions(); if (optionsPath != null) { - var op = Uris.parseOrNull(optionsPath).toURL().getPath(); - CqlTranslatorOptions options = CqlTranslatorOptionsMapper.fromFile(op); + var op = Path(Uris.parseOrNull(optionsPath).toURL().getPath()); + CqlTranslatorOptions options = CqlTranslatorOptions.fromFile(Path(op)); cqlOptions.setCqlCompilerOptions(options.getCqlCompilerOptions()); } @@ -211,24 +208,34 @@ public Integer call() throws Exception { evaluationSettings.setNpmProcessor(new NpmProcessor(igContext)); for (LibraryParameter library : libraries) { - var libraryPath = Paths.get(Uris.parseOrNull(library.libraryUrl)); - - var modelPath = library.model != null ? Paths.get(Uris.parseOrNull(library.model.modelUrl)) : null; - - var terminologyPath = - library.terminologyUrl != null ? Paths.get(Uris.parseOrNull(library.terminologyUrl)) : null; + // Paths are mixed types + // IgStandardRepository used java nio path objects + // DefaultLibraryServiceProvider used kotlin path objects + // Until the language server can be ported to kotlin, the differences will exist + var libraryKotlinPath = library.libraryUrl != null + ? Path(Uris.parseOrNull(library.libraryUrl).toURL().getPath()) + : null; + + var modelPath = library.model != null + ? Paths.get(Uris.parseOrNull(library.model.modelUrl).toURL().getPath()) + : null; + + var terminologyPath = library.terminologyUrl != null + ? Paths.get(Uris.parseOrNull(library.terminologyUrl).toURL().getPath()) + : null; var repository = createRepository(fhirContext, terminologyPath, modelPath); + var engine = Engines.forRepository(repository, evaluationSettings); if (library.libraryUrl != null) { - var provider = new DefaultLibrarySourceProvider(libraryPath); + var provider = new DefaultLibrarySourceProvider(libraryKotlinPath); engine.getEnvironment() .getLibraryManager() .getLibrarySourceLoader() .registerProvider(provider); - var modelProvider = new DefaultModelInfoProvider(libraryPath); + var modelProvider = new DefaultModelInfoProvider(libraryKotlinPath); engine.getEnvironment() .getLibraryManager() .getModelManager() @@ -261,13 +268,13 @@ private IRepository createRepository(FhirContext fhirContext, Path terminologyPa } if (modelPath != null) { - data = new IgRepository(fhirContext, modelPath); + data = new IgStandardRepository(fhirContext, modelPath); } else { data = new NoOpRepository(fhirContext); } if (terminologyPath != null) { - terminology = new IgRepository(fhirContext, terminologyPath); + terminology = new IgStandardRepository(fhirContext, terminologyPath); } else { terminology = new NoOpRepository(fhirContext); } @@ -277,7 +284,8 @@ private IRepository createRepository(FhirContext fhirContext, Path terminologyPa @SuppressWarnings("java:S106") // We are intending to output to the console here as a CLI tool private void writeResult(EvaluationResult result) { - for (Map.Entry libraryEntry : result.expressionResults.entrySet()) { + for (Map.Entry libraryEntry : + result.getExpressionResults().entrySet()) { System.out.println(libraryEntry.getKey() + "=" + this.tempConvert(libraryEntry.getValue().value())); } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.java index 5901f2ac..73ceb49d 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.java @@ -2,14 +2,12 @@ import com.google.gson.JsonElement; import java.io.IOException; -import java.io.StringWriter; import java.net.URI; import java.util.Collections; import java.util.Set; import java.util.concurrent.CompletableFuture; import org.cqframework.cql.cql2elm.CqlCompiler; -import org.cqframework.cql.cql2elm.LibraryContentType; -import org.cqframework.cql.elm.serializing.ElmLibraryWriterFactory; +import org.cqframework.cql.elm.serializing.ElmXmlLibraryWriter; import org.eclipse.lsp4j.ExecuteCommandParams; import org.hl7.elm.r1.Library; import org.opencds.cqf.cql.ls.core.utility.Uris; @@ -61,8 +59,7 @@ private CompletableFuture viewElm(ExecuteCommandParams params) { } private static String convertToXml(Library library) throws IOException { - StringWriter writer = new StringWriter(); - ElmLibraryWriterFactory.getWriter(LibraryContentType.XML.mimeType()).write(library, writer); - return writer.getBuffer().toString(); + ElmXmlLibraryWriter writer = new ElmXmlLibraryWriter(); + return writer.writeAsString(library); } } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CacheAwareModelManager.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CacheAwareModelManager.java deleted file mode 100644 index e44ff0f1..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CacheAwareModelManager.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.opencds.cqf.cql.ls.server.manager; - -import java.util.Map; -import org.cqframework.cql.cql2elm.ModelManager; -import org.cqframework.cql.cql2elm.model.Model; -import org.hl7.cql.model.ModelIdentifier; - -/** - * This class extends the CQL translator {@link org.cqframework.cql.cql2elm.ModelManager} class to be aware of a global cache of {@link org.cqframework.cql.cql2elm.model.Model}s - * The global cache is by @{org.hl7.cql.model.ModelIdentifier}, while the local cache is by name. This is because the translator expects the ModelManager to only permit loading - * of a single version version of a given Model in a single translation context, while the global cache is for all versions of Models - * - * As of 2.3.0, the translator ModelManager has incorporated support for the use of the global cache by ModelIdentifier, so this - * class is now a backwards-compatibility wrapper for that functionality and should be deprecated. - */ -public class CacheAwareModelManager extends ModelManager { - /** - * @param globalCache cache for Models by ModelIdentifier. Expected to be thread-safe. - */ - public CacheAwareModelManager(Map globalCache) { - super(globalCache); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.java index a2b611da..336f61ae 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.java @@ -1,13 +1,13 @@ package org.opencds.cqf.cql.ls.server.manager; +import static kotlinx.io.files.PathsKt.Path; + import java.io.InputStream; -import java.io.InputStreamReader; import java.net.URI; import java.util.HashMap; import java.util.Map; import org.cqframework.cql.cql2elm.CqlCompilerOptions; import org.cqframework.cql.cql2elm.CqlTranslatorOptions; -import org.cqframework.cql.cql2elm.CqlTranslatorOptionsMapper; import org.cqframework.cql.cql2elm.LibraryBuilder.SignatureLevel; import org.eclipse.lsp4j.FileEvent; import org.greenrobot.eventbus.Subscribe; @@ -47,8 +47,7 @@ protected CqlCompilerOptions readOptions(URI rootUri) { InputStream input = contentService.read(Uris.addPath(rootUri, "/cql-options.json")); if (input != null) { - options = CqlTranslatorOptionsMapper.fromReader(new InputStreamReader(input)) - .getCqlCompilerOptions(); + options = CqlTranslatorOptions.fromFile(Path("/cql-options.json")).getCqlCompilerOptions(); } else { log.info("cql-options.json not found, using default options"); options = CqlTranslatorOptions.defaultOptions().getCqlCompilerOptions(); diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManager.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManager.java index b2361aee..82a6f5c1 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManager.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManager.java @@ -14,6 +14,7 @@ import org.fhir.ucum.UcumService; import org.hl7.cql.model.ModelIdentifier; import org.opencds.cqf.cql.ls.core.ContentService; +import org.opencds.cqf.cql.ls.core.utility.Converters; import org.opencds.cqf.cql.ls.core.utility.Uris; import org.opencds.cqf.cql.ls.server.provider.ContentServiceModelInfoProvider; import org.opencds.cqf.cql.ls.server.provider.ContentServiceSourceProvider; @@ -67,8 +68,8 @@ public CqlCompiler compile(URI uri, InputStream stream) { LibraryManager libraryManager = this.createLibraryManager(Uris.getHead(uri), modelManager); try { - CqlCompiler compiler = new CqlCompiler(libraryManager); - compiler.run(stream); + CqlCompiler compiler = new CqlCompiler(null, null, libraryManager); + compiler.run(Converters.inputStreamToString(stream)); return compiler; } catch (IOException e) { throw new IllegalArgumentException(String.format("error creating compiler for uri: %s", uri.toString()), e); @@ -76,7 +77,7 @@ public CqlCompiler compile(URI uri, InputStream stream) { } private ModelManager createModelManager() { - return new CacheAwareModelManager(this.globalCache); + return new ModelManager(this.globalCache); } private LibraryManager createLibraryManager(URI root, ModelManager modelManager) { diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProvider.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProvider.java index 678a6bf2..99b08449 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProvider.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProvider.java @@ -5,8 +5,9 @@ import org.hl7.cql.model.ModelIdentifier; import org.hl7.cql.model.ModelInfoProvider; import org.hl7.elm_modelinfo.r1.ModelInfo; -import org.hl7.elm_modelinfo.r1.serializing.ModelInfoReaderFactory; +import org.hl7.elm_modelinfo.r1.serializing.XmlModelInfoReaderKt; import org.opencds.cqf.cql.ls.core.ContentService; +import org.opencds.cqf.cql.ls.core.utility.Converters; import org.opencds.cqf.cql.ls.core.utility.Uris; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,24 +37,9 @@ public ModelInfo load(ModelIdentifier modelIdentifier) { modelName.toLowerCase(), modelVersion != null ? ("-" + modelVersion) : "")); InputStream modelInputStream = contentService.read(modelUri); if (modelInputStream != null) { - return ModelInfoReaderFactory.getReader("application/xml").read(modelInputStream); + return XmlModelInfoReaderKt.parseModelInfoXml(Converters.inputStreamToString(modelInputStream)); } - } catch (IOException e) { - throw new IllegalArgumentException( - String.format("Could not load definition for model info %s.", modelIdentifier.getId()), e); - } - - try { - URI modelUri = Uris.addPath( - root, - String.format( - "/%s-modelinfo%s.json", - modelName.toLowerCase(), modelVersion != null ? ("-" + modelVersion) : "")); - InputStream modelInputStream = contentService.read(modelUri); - if (modelInputStream != null) { - return ModelInfoReaderFactory.getReader("application/json").read(modelInputStream); - } - } catch (IOException e) { + } catch (Exception e) { throw new IllegalArgumentException( String.format("Could not load definition for model info %s.", modelIdentifier.getId()), e); } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.java index 445f229c..c9e17b46 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.java @@ -1,7 +1,10 @@ package org.opencds.cqf.cql.ls.server.provider; -import java.io.InputStream; +import static org.opencds.cqf.cql.ls.core.utility.Converters.inputStreamToSource; + +import java.io.IOException; import java.net.URI; +import kotlinx.io.Source; import org.cqframework.cql.cql2elm.LibrarySourceProvider; import org.hl7.elm.r1.VersionedIdentifier; import org.opencds.cqf.cql.ls.core.ContentService; @@ -16,8 +19,11 @@ public ContentServiceSourceProvider(URI root, ContentService contentService) { this.root = root; } - @Override - public InputStream getLibrarySource(VersionedIdentifier libraryIdentifier) { - return this.contentService.read(this.root, libraryIdentifier); + public Source getLibrarySource(VersionedIdentifier libraryIdentifier) { + try { + return inputStreamToSource(this.contentService.read(this.root, libraryIdentifier)); + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/FormattingProvider.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/FormattingProvider.java index 5c2cb7f8..00466f56 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/FormattingProvider.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/FormattingProvider.java @@ -14,7 +14,7 @@ import org.opencds.cqf.cql.ls.core.utility.Uris; public class FormattingProvider { - private ContentService contentService; + private final ContentService contentService; public FormattingProvider(ContentService contentService) { this.contentService = contentService; @@ -23,9 +23,9 @@ public FormattingProvider(ContentService contentService) { public List format(String uri) { URI u = Objects.requireNonNull(Uris.parseOrNull(uri)); - FormatResult fr = null; + FormatResult fr; try { - fr = CqlFormatterVisitor.getFormattedOutput(this.contentService.read(u)); + fr = CqlFormatterVisitor.Companion.getFormattedOutput(this.contentService.read(u)); } catch (IOException e) { throw new IllegalArgumentException("Unable to format CQL due to an error.", e); } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/HoverProvider.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/HoverProvider.java index f9c3472a..58320c30 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/HoverProvider.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/HoverProvider.java @@ -3,7 +3,8 @@ import java.net.URI; import org.apache.commons.lang3.tuple.Pair; import org.cqframework.cql.cql2elm.CqlCompiler; -import org.cqframework.cql.elm.tracking.TrackBack; +import org.cqframework.cql.cql2elm.tracking.TrackBack; +import org.cqframework.cql.cql2elm.tracking.Trackable; import org.eclipse.lsp4j.*; import org.hl7.cql.model.DataType; import org.hl7.elm.r1.ExpressionDef; @@ -12,7 +13,7 @@ import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; public class HoverProvider { - private CqlCompilationManager cqlCompilationManager; + private final CqlCompilationManager cqlCompilationManager; public HoverProvider(CqlCompilationManager cqlCompilationManager) { this.cqlCompilationManager = cqlCompilationManager; @@ -43,8 +44,7 @@ public Hover hover(HoverParams position) { // For that given position, we want to select the most specific node we support generating // hover information for and return that. // - // (maybe.. the alternative is to select the specific node under the cursor, but that may be - // less user friendly) + // (maybe the alternative is to select the specific node under the cursor, but that may be less user-friendly) // // The current code always picks the first ExpressionDef in the graph. Pair exp = getExpressionDefForPosition( @@ -64,18 +64,15 @@ public Hover hover(HoverParams position) { } private Pair getExpressionDefForPosition(Position position, Statements statements) { - if (statements == null - || statements.getDef() == null - || statements.getDef().isEmpty()) { + if (statements == null || statements.getDef().isEmpty()) { return null; } - for (ExpressionDef def : statements.getDef()) { - if (def.getTrackbacks() == null || def.getTrackbacks().isEmpty()) { + if (Trackable.INSTANCE.getTrackbacks(def).isEmpty()) { continue; } - for (TrackBack tb : def.getTrackbacks()) { + for (TrackBack tb : Trackable.INSTANCE.getTrackbacks(def)) { if (positionInTrackBack(position, tb)) { Range range = new Range( new Position(tb.getStartLine() - 1, tb.getStartChar() - 1), @@ -105,7 +102,7 @@ public MarkupContent markup(ExpressionDef def) { return null; } - DataType resultType = def.getExpression().getResultType(); + DataType resultType = Trackable.INSTANCE.getResultType(def); if (resultType == null) { return null; } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardConventions.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardConventions.java new file mode 100644 index 00000000..7d46c7e7 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardConventions.java @@ -0,0 +1,251 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import jakarta.annotation.Nonnull; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.hl7.fhir.r4.model.Enumerations.FHIRAllTypes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class represents the different file structures for an IG repository. The main differences + * between the various configurations are whether the files are organized by resource type + * and/or category, and whether the files are prefixed with the resource type. + */ +public record IgStandardConventions( + FhirTypeLayout typeLayout, + CategoryLayout categoryLayout, + CompartmentLayout compartmentLayout, + FilenameMode filenameMode) { + + private static final Logger logger = LoggerFactory.getLogger(IgStandardConventions.class); + + public enum FhirTypeLayout { + DIRECTORY_PER_TYPE, + FLAT + } + + public enum CategoryLayout { + DIRECTORY_PER_CATEGORY, + FLAT + } + + public enum CompartmentLayout { + DIRECTORY_PER_COMPARTMENT, + FLAT + } + + public enum FilenameMode { + TYPE_AND_ID, + ID_ONLY + } + + public static final IgStandardConventions FLAT = new IgStandardConventions( + FhirTypeLayout.FLAT, CategoryLayout.FLAT, CompartmentLayout.FLAT, FilenameMode.TYPE_AND_ID); + + public static final IgStandardConventions STANDARD = new IgStandardConventions( + FhirTypeLayout.DIRECTORY_PER_TYPE, + CategoryLayout.DIRECTORY_PER_CATEGORY, + CompartmentLayout.FLAT, + FilenameMode.ID_ONLY); + + private static final List FHIR_TYPE_NAMES = Stream.of(FHIRAllTypes.values()) + .map(FHIRAllTypes::name) + .map(String::toLowerCase) + .distinct() + .toList(); + + /** + * Auto-detect the IG conventions based on the structure of the IG. If the path is null or the + * convention can not be reliably detected, the default configuration is returned. + * + * @param path The path to the IG. + * @return The IG conventions. + */ + public static IgStandardConventions autoDetect(Path path) { + + if (path == null || !Files.exists(path)) { + return STANDARD; + } + + // A "category" hierarchy may exist in the ig file structure, + // where resource categories ("data", "terminology", "content") are organized into + // subdirectories ("tests", "vocabulary", "resources"). + // + // e.g. "input/tests", "input/vocabulary". + // + // Check all possible category paths and grab the first that exists, + // or use the IG path if none exist. + + var categoryPath = Stream.of("tests", "vocabulary", "resources") + .map(path::resolve) + .filter(x -> x.toFile().exists()) + .findFirst() + .orElse(path); + + var hasCategoryDirectory = !path.equals(categoryPath); + + var hasCompartmentDirectory = false; + + // Compartments can only exist for test data + if (hasCategoryDirectory) { + var tests = path.resolve("tests"); + // A compartment under the tests looks like a set of subdirectories + // e.g. "input/tests/Patient", "input/tests/Practitioner" + // that themselves contain subdirectories for each test case. + // e.g. "input/tests/Patient/test1", "input/tests/Patient/test2" + // Then within those, the structure may be flat (e.g. "input/tests/Patient/test1/123.json") + // or grouped by type (e.g. "input/tests/Patient/test1/Patient/123.json"). + // + // The trick is that the in the case that the test cases are + // grouped by type, the compartment directory will be the same as the type directory. + // so we need to look at the resource type directory and check if the contents are files + // or more directories. If more directories exist, and the directory name is not a + // FHIR type, then we have a compartment directory. + if (tests.toFile().exists()) { + var compartments = FHIR_TYPE_NAMES.stream().map(tests::resolve).filter(x -> x.toFile() + .exists()); + + final List compartmentsList = compartments.toList(); + + // Check if any of the potential compartment directories + // have subdirectories that are not FHIR types (e.g. "input/tests/Patient/test1). + hasCompartmentDirectory = compartmentsList.stream() + .flatMap(IgStandardConventions::listFiles) + .filter(Files::isDirectory) + .anyMatch(IgStandardConventions::matchesAnyResource); + } + } + + // A "type" may also exist in the igs file structure, where resources + // are grouped by type into subdirectories. + // + // e.g. "input/vocabulary/valueset", "input/resources/valueset". + // + // Check all possible type paths and grab the first that exists, + // or use the category directory if none exist + var typePath = FHIR_TYPE_NAMES.stream() + .map(categoryPath::resolve) + .filter(Files::exists) + .findFirst() + .orElse(categoryPath); + + var hasTypeDirectory = !categoryPath.equals(typePath); + + // A file "claims" to be a FHIR resource type if its filename starts with a valid FHIR type name. + // For files that "claim" to be a FHIR resource type, we check to see if the contents of the file + // have a resource that matches the claimed type. + var hasTypeFilename = hasTypeFilename(typePath); + + var config = new IgStandardConventions( + hasTypeDirectory ? FhirTypeLayout.DIRECTORY_PER_TYPE : FhirTypeLayout.FLAT, + hasCategoryDirectory ? CategoryLayout.DIRECTORY_PER_CATEGORY : CategoryLayout.FLAT, + hasCompartmentDirectory ? CompartmentLayout.DIRECTORY_PER_COMPARTMENT : CompartmentLayout.FLAT, + hasTypeFilename ? FilenameMode.TYPE_AND_ID : FilenameMode.ID_ONLY); + + logger.info("Auto-detected repository configuration: {}", config); + + return config; + } + + private static boolean hasTypeFilename(Path typePath) { + try (var fileStream = Files.list(typePath)) { + return fileStream + .filter(IgStandardConventions::fileNameMatchesType) + .filter(filePath -> claimedFhirType(filePath) != FHIRAllTypes.NULL) + .anyMatch(filePath -> contentsMatchClaimedType(filePath, claimedFhirType(filePath))); + } catch (IOException exception) { + logger.error("Error listing files in path: {}", typePath, exception); + return false; + } + } + + private static boolean fileNameMatchesType(Path innerFile) { + Objects.requireNonNull(innerFile); + var fileName = innerFile.getFileName().toString(); + return FHIR_TYPE_NAMES.stream().anyMatch(type -> fileName.toLowerCase().startsWith(type)); + } + + private static boolean matchesAnyResource(Path innerFile) { + return !FHIR_TYPE_NAMES.contains(innerFile.getFileName().toString().toLowerCase()); + } + + @Nonnull + private static Stream listFiles(Path innerPath) { + try { + return Files.list(innerPath); + } catch (IOException e) { + logger.error("Error listing files in path: {}", innerPath, e); + return Stream.empty(); + } + } + + // This method checks to see if the contents of a file match the type claimed by the filename + private static boolean contentsMatchClaimedType(Path filePath, FHIRAllTypes claimedFhirType) { + Objects.requireNonNull(filePath); + Objects.requireNonNull(claimedFhirType); + + try (var linesStream = Files.lines(filePath, StandardCharsets.UTF_8)) { + var contents = linesStream.collect(Collectors.joining()); + if (contents.isEmpty()) { + return false; + } + + var filename = filePath.getFileName().toString(); + var fileNameWithoutExtension = filename.substring(0, filename.lastIndexOf(".")); + // Check that the contents contain the claimed type, and that the id is not the same as the filename + // NOTE: This does not work for XML files. + return contents.toUpperCase().contains("\"RESOURCETYPE\": \"%s\"".formatted(claimedFhirType.name())) + && !contents.toUpperCase() + .contains("\"ID\": \"%s\"".formatted(fileNameWithoutExtension.toUpperCase())); + + } catch (IOException e) { + return false; + } + } + + // Detects the FHIR type claimed by the filename + private static FHIRAllTypes claimedFhirType(Path filePath) { + var filename = filePath.getFileName().toString(); + if (!filename.contains("-")) { + return FHIRAllTypes.NULL; + } + + var codeName = filename.substring(0, filename.indexOf("-")).toUpperCase(); + try { + return FHIRAllTypes.valueOf(codeName); + } catch (Exception e) { + return FHIRAllTypes.NULL; + } + } + + @Override + public boolean equals(Object other) { + if (other == null || getClass() != other.getClass()) { + return false; + } + IgStandardConventions that = (IgStandardConventions) other; + return typeLayout == that.typeLayout + && filenameMode == that.filenameMode + && categoryLayout == that.categoryLayout + && compartmentLayout == that.compartmentLayout; + } + + @Override + public int hashCode() { + return Objects.hash(typeLayout, categoryLayout, compartmentLayout, filenameMode); + } + + @Override + @Nonnull + public String toString() { + return "IGConventions [typeLayout=%s, categoryLayout=%s compartmentLayout=%s, filenameMode=%s]" + .formatted(typeLayout, categoryLayout, compartmentLayout, filenameMode); + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardCqlContent.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardCqlContent.java new file mode 100644 index 00000000..13cd2d62 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardCqlContent.java @@ -0,0 +1,73 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static java.util.Objects.requireNonNull; + +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.BiConsumer; +import java.util.function.Function; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class IgStandardCqlContent { + + private IgStandardCqlContent() { + // intentionally empty + } + + public static void loadCqlContent(IBaseResource resource, Path resourcePath) { + requireNonNull(resource, "resource can not be null"); + requireNonNull(resourcePath, "resourcePath can not be null"); + + if (!"Library".equals(resource.fhirType())) { + return; + } + + Function cqlPathExtractor = null; + BiConsumer cqlContentAttacher = null; + switch (resource.getStructureFhirVersionEnum()) { + case DSTU3: + cqlPathExtractor = org.opencds.cqf.fhir.utility.dstu3.AttachmentUtil::getCqlLocation; + cqlContentAttacher = org.opencds.cqf.fhir.utility.dstu3.AttachmentUtil::addData; + break; + case R4: + cqlPathExtractor = org.opencds.cqf.fhir.utility.r4.AttachmentUtil::getCqlLocation; + cqlContentAttacher = org.opencds.cqf.fhir.utility.r4.AttachmentUtil::addData; + break; + case R5: + cqlPathExtractor = org.opencds.cqf.fhir.utility.r5.AttachmentUtil::getCqlLocation; + cqlContentAttacher = org.opencds.cqf.fhir.utility.r5.AttachmentUtil::addData; + break; + default: + throw new IllegalArgumentException( + "Unsupported FHIR version: %s".formatted(resource.getStructureFhirVersionEnum())); + } + + readAndAttachCqlContent(resource, resourcePath, cqlPathExtractor, cqlContentAttacher); + } + + private static void readAndAttachCqlContent( + IBaseResource resource, + Path resourcePath, + Function cqlPathExtractor, + BiConsumer cqlContentAttacher) { + String cqlPath = cqlPathExtractor.apply(resource); + if (cqlPath == null) { + return; + } + + String cqlContent = getCqlContent(resourcePath, cqlPath); + cqlContentAttacher.accept(resource, cqlContent); + } + + static String getCqlContent(Path rootPath, String relativePath) { + var path = rootPath.resolve(relativePath).normalize(); + try { + return new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new ResourceNotFoundException("Unable to read CQL content from path: %s".formatted(path)); + } + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardEncodingBehavior.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardEncodingBehavior.java new file mode 100644 index 00000000..9c402f73 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardEncodingBehavior.java @@ -0,0 +1,41 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import ca.uhn.fhir.rest.api.EncodingEnum; + +/** + * This class is used to determine how to handle encoding when reading and writing resources. You can + * choose to preserve the encoding of the resource when reading and writing, or you can choose to change + * the encoding to a preferred encoding when writing. New resources will always be written in the preferred + * encoding. + */ +public class IgStandardEncodingBehavior { + + /** + * When updating a resource, you can choose to preserve the original encoding of the resource + * or you can choose to overwrite the original encoding with the preferred encoding. + */ + public enum PreserveEncoding { + PRESERVE_ORIGINAL_ENCODING, + OVERWRITE_WITH_PREFERRED_ENCODING + } + + public static final IgStandardEncodingBehavior DEFAULT = new IgStandardEncodingBehavior( + EncodingEnum.JSON, IgStandardEncodingBehavior.PreserveEncoding.PRESERVE_ORIGINAL_ENCODING); + + private final EncodingEnum preferredEncoding; + private final IgStandardEncodingBehavior.PreserveEncoding preserveEncoding; + + public IgStandardEncodingBehavior( + EncodingEnum preferredEncoding, IgStandardEncodingBehavior.PreserveEncoding preserveEncoding) { + this.preferredEncoding = preferredEncoding; + this.preserveEncoding = preserveEncoding; + } + + EncodingEnum preferredEncoding() { + return preferredEncoding; + } + + PreserveEncoding preserveEncoding() { + return preserveEncoding; + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.java new file mode 100644 index 00000000..ef1406b8 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.java @@ -0,0 +1,906 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static java.util.Objects.requireNonNull; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; +import ca.uhn.fhir.util.BundleBuilder; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Multimap; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.matcher.ResourceMatcher; +import org.opencds.cqf.fhir.utility.repository.IRepositoryOperationProvider; +import org.opencds.cqf.fhir.utility.repository.Repositories; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides access to FHIR resources stored in a directory structure following + * Implementation Guide (IG) conventions. + * Supports CRUD operations and resource management based on IG directory and + * file naming conventions. + * + *

+ * Directory Structure Overview (based on conventions): + *

+ * + *
+ * /path/to/ig/root/          (CategoryLayout.FLAT)
+ * ├── Patient-001.json
+ * ├── Observation-002.json
+ * ├── or
+ * ├── [resources/]             (CategoryLayout.DIRECTORY_PER_CATEGORY)
+ * │   ├── Patient-789.json       (FhirTypeLayout.FLAT)
+ * │   ├── or
+ * │   ├── [patient/]           (FhirTypeLayout.DIRECTORY_PER_TYPE)
+ * │   │   ├── Patient-123.json   (FilenameMode.TYPE_AND_ID)
+ * │   │   ├── or
+ * │   │   ├── 456.json           (FilenameMode.ID_ONLY)
+ * │   │   └── ...
+ * │   └── ...
+ * └── vocabulary/              (CategoryLayout.DIRECTORY_PER_CATEGORY)
+ *     ├── ValueSet-abc.json
+ *     ├── def.json
+ *     └── external/            (External Resources - Read-only, Terminology-only)
+ *         └── CodeSystem-external.json
+ * 
+ *

+ * Key Features: + *

+ *
    + *
  • Supports CRUD operations on FHIR resources.
  • + *
  • Handles different directory layouts and filename conventions based on IG + * conventions.
  • + *
  • Annotates resources with metadata like source path and external + * designation.
  • + *
  • Supports invoking FHIR operations through an + * {@link IRepositoryOperationProvider}.
  • + *
  • Utilizes caching for efficient resource access.
  • + *
+ */ +public class IgStandardRepository implements IRepository { + private static final Logger log = LoggerFactory.getLogger(IgStandardRepository.class); + + private final FhirContext fhirContext; + private final Path root; + private final IgStandardConventions conventions; + private final IgStandardEncodingBehavior encodingBehavior; + private final ResourceMatcher resourceMatcher; + private IRepositoryOperationProvider operationProvider; + + private final Cache resourceCache = + CacheBuilder.newBuilder().concurrencyLevel(10).maximumSize(500).build(); + + // Metadata fields attached to resources that are read from the repository + // These fields are used to determine if a resource is external, and to + // maintain the original encoding of the resource. + static final String SOURCE_PATH_TAG = "sourcePath"; // Path + + // Directory names + static final String EXTERNAL_DIRECTORY = "external"; + static final Map CATEGORY_DIRECTORIES = new ImmutableMap.Builder< + IgStandardResourceCategory, String>() + .put(IgStandardResourceCategory.CONTENT, "resources") + .put(IgStandardResourceCategory.DATA, "tests") + .put(IgStandardResourceCategory.TERMINOLOGY, "vocabulary") + .build(); + + static final BiMap FILE_EXTENSIONS = new ImmutableBiMap.Builder() + .put(EncodingEnum.JSON, "json") + .put(EncodingEnum.XML, "xml") + .put(EncodingEnum.RDF, "rdf") + .build(); + + // This header to used so that the user can pass current compartment context + // to the repository. Basically, this will affect how the repository will do reads/writes + // The expected format for this header is: ResourceType/Id (e.g. Patient/123) + public static final String FHIR_COMPARTMENT_HEADER = "X-FHIR-Compartment"; + + private static IParser parserForEncoding(FhirContext fhirContext, EncodingEnum encodingEnum) { + return switch (encodingEnum) { + case JSON -> fhirContext.newJsonParser(); + case XML -> fhirContext.newXmlParser(); + case RDF -> fhirContext.newRDFParser(); + default -> throw new IllegalArgumentException("NDJSON is not supported"); + }; + } + + /** + * Creates a new {@code IgRepository} with auto-detected conventions and default + * encoding behavior. + * The repository configuration is determined based on the directory structure. + * + * @param fhirContext The FHIR context to use for parsing and encoding + * resources. + * @param root The root directory of the IG. + * @see IgStandardConventions#autoDetect(Path) + */ + public IgStandardRepository(FhirContext fhirContext, Path root) { + this(fhirContext, root, IgStandardConventions.autoDetect(root), IgStandardEncodingBehavior.DEFAULT, null); + } + + /** + * Creates a new {@code IgRepository} with specified conventions and encoding + * behavior. + * + * @param fhirContext The FHIR context to use. + * @param root The root directory of the IG. + * @param conventions The conventions defining directory and filename + * structures. + * @param encodingBehavior The encoding behavior for parsing and encoding + * resources. + * @param operationProvider The operation provider for invoking FHIR operations. + */ + public IgStandardRepository( + FhirContext fhirContext, + Path root, + IgStandardConventions conventions, + IgStandardEncodingBehavior encodingBehavior, + IRepositoryOperationProvider operationProvider) { + this.fhirContext = requireNonNull(fhirContext, "fhirContext cannot be null"); + this.root = requireNonNull(root, "root cannot be null"); + this.conventions = requireNonNull(conventions, "conventions is required"); + this.encodingBehavior = requireNonNull(encodingBehavior, "encodingBehavior is required"); + this.resourceMatcher = Repositories.getResourceMatcher(this.fhirContext); + this.operationProvider = operationProvider; + } + + public void setOperationProvider(IRepositoryOperationProvider operationProvider) { + this.operationProvider = operationProvider; + } + + public void clearCache() { + this.resourceCache.invalidateAll(); + } + + private boolean isExternalPath(Path path) { + return path.getParent() != null + && path.getParent().toString().toLowerCase().endsWith(EXTERNAL_DIRECTORY); + } + + /** + * Determines the preferred file system path for storing or retrieving a FHIR + * resource based on its resource type and identifier. + * + *

+ * Example (based on conventions): + *

+ * + *
+     * /path/to/ig/root/[[resources/]][[patient/]]Patient-123.json
+     * 
+ * + * - The presence of `resources/` depends on + * `CategoryLayout.DIRECTORY_PER_CATEGORY`. + * - The presence of `patient/` depends on `FhirTypeLayout.DIRECTORY_PER_TYPE`. + * - The filename format depends on `FilenameMode`: + * - `TYPE_AND_ID`: `Patient-123.json` + * - `ID_ONLY`: `123.json` + * + * @param The type of the FHIR resource. + * @param The type of the resource identifier. + * @param resourceType The class representing the FHIR resource type. + * @param id The identifier of the resource. + * @return The {@code Path} representing the preferred location for the + * resource. + */ + protected Path preferredPathForResource( + Class resourceType, I id, IgStandardRepositoryCompartment igRepositoryCompartment) { + var directory = directoryForResource(resourceType, igRepositoryCompartment); + var fileName = fileNameForResource( + resourceType.getSimpleName(), id.getIdPart(), this.encodingBehavior.preferredEncoding()); + return directory.resolve(fileName); + } + + /** + * Generates all possible file paths where a resource might be found. + * + * @param The type of the FHIR resource. + * @param The type of the resource identifier. + * @param resourceType The class representing the FHIR resource type. + * @param id The identifier of the resource. + * @param igRepositoryCompartment The compartment context to use + * @return A list of potential paths for the resource. + */ + protected List potentialPathsForResource( + Class resourceType, I id, IgStandardRepositoryCompartment igRepositoryCompartment) { + + var potentialDirectories = new ArrayList(); + var directory = directoryForResource(resourceType, igRepositoryCompartment); + potentialDirectories.add(directory); + + // Currently, only terminology resources are allowed to be external + if (IgStandardResourceCategory.forType(resourceType.getSimpleName()) + == IgStandardResourceCategory.TERMINOLOGY) { + var externalDirectory = directory.resolve(EXTERNAL_DIRECTORY); + potentialDirectories.add(externalDirectory); + } + + var potentialPaths = new ArrayList(); + + for (var dir : potentialDirectories) { + for (var encoding : FILE_EXTENSIONS.keySet()) { + potentialPaths.add( + dir.resolve(fileNameForResource(resourceType.getSimpleName(), id.getIdPart(), encoding))); + } + } + + return potentialPaths; + } + + /** + * Constructs the filename based on conventions: + * - ID_ONLY: "123.json" + * - TYPE_AND_ID: "Patient-123.json" + * + * @param resourceType The resource type (e.g., "Patient"). + * @param resourceId The resource ID (e.g., "123"). + * @param encoding The encoding (e.g., JSON). + * @return The filename. + */ + protected String fileNameForResource(String resourceType, String resourceId, EncodingEnum encoding) { + var name = resourceId + "." + FILE_EXTENSIONS.get(encoding); + if (IgStandardConventions.FilenameMode.ID_ONLY.equals(conventions.filenameMode())) { + return name; + } else { + return resourceType + "-" + name; + } + } + + /** + * Determines the directory path for a resource category. + * + * - `CategoryLayout.FLAT`: Returns the root directory. + * - `CategoryLayout.DIRECTORY_PER_CATEGORY`: Returns the category-specific + * subdirectory (e.g., `/resources/`). + * + * @param The type of the FHIR resource. + * @param resourceType The class representing the FHIR resource type. + * @param igStandardRepositoryCompartment The compartment context to use + * @return The path representing the directory for the resource category. + */ + protected Path directoryForCategory( + Class resourceType, IgStandardRepositoryCompartment igStandardRepositoryCompartment) { + if (this.conventions.categoryLayout() == IgStandardConventions.CategoryLayout.FLAT) { + return this.root; + } + + var category = IgStandardResourceCategory.forType(resourceType.getSimpleName()); + var directory = CATEGORY_DIRECTORIES.get(category); + var categoryPath = root.resolve(directory); + + if (this.conventions.compartmentLayout() == IgStandardConventions.CompartmentLayout.DIRECTORY_PER_COMPARTMENT + && !igStandardRepositoryCompartment.isEmpty()) { + // Compartment directories are only for DATA resources (e.g., Patient, Encounter) + // and are placed directly under the category directory. + if (category == IgStandardResourceCategory.DATA) { + return categoryPath.resolve(pathForCompartment(igStandardRepositoryCompartment)); + } + } + + return categoryPath; + } + + /** + * Determines the directory path for a resource type. + * - If `FhirTypeLayout.FLAT`, returns the base directory (could be root or + * category directory). + * - If `FhirTypeLayout.DIRECTORY_PER_TYPE`, returns the type-specific + * subdirectory within the base directory. + * + *

+ * Example (based on `FhirTypeLayout`): + *

+ * + *
+     * /path/to/ig/root/[[patient/]]
+     * 
+ * + * - `[[patient/]]` is present if `FhirTypeLayout.DIRECTORY_PER_TYPE` is used. + * + * @param The type of the FHIR resource. + * @param resourceType The class representing the FHIR resource type. + * @param igRepositoryCompartment The compartment context to use + * @return The path representing the directory for the resource type. + */ + protected Path directoryForResource( + Class resourceType, IgStandardRepositoryCompartment igRepositoryCompartment) { + var directory = directoryForCategory(resourceType, igRepositoryCompartment); + if (this.conventions.typeLayout() == IgStandardConventions.FhirTypeLayout.FLAT) { + return directory; + } + + return directory.resolve(resourceType.getSimpleName().toLowerCase()); + } + + /** + * Reads a resource from the given file path. + * + * @param path The path to the resource file. + * @return An {@code Optional} containing the resource if found; otherwise, + * empty. + */ + // @Nullable + protected IBaseResource readResource(Path path) { + log.info("IgStandardRepository.readResource - Attempting to read resource from path: {}", path); + var file = path.toFile(); + if (!file.exists()) { + log.info("IgStandardRepository.readResource - Didn't find file"); + return null; + } + + var extension = fileExtension(path); + if (extension == null) { + log.info("IgStandardRepository.readResource - Extension check failed"); + return null; + } + + var encoding = FILE_EXTENSIONS.inverse().get(extension); + + try { + String s = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + var resource = parserForEncoding(fhirContext, encoding).parseResource(s); + resource.setUserData(SOURCE_PATH_TAG, path); + IgStandardCqlContent.loadCqlContent(resource, path.getParent()); + + log.info("IgStandardRepository.readResource - Returning resource: {}", resource); + return resource; + } catch (FileNotFoundException e) { + return null; + } catch (DataFormatException e) { + throw new ResourceNotFoundException("Found empty or invalid content at path %s".formatted(path)); + } catch (IOException e) { + throw new UnclassifiedServerFailureException(500, "Unable to read resource from path %s".formatted(path)); + } + } + + protected IBaseResource cachedReadResource(Path path) { + var o = this.resourceCache.getIfPresent(path); + if (o != null) { + log.info("IgStandardRepository.cachedReadResource - Returning cached resource: {}", o); + return o; + } else { + var resource = readResource(path); + this.resourceCache.put(path, resource); + log.info("IgStandardRepository.cachedReadResource - Returning freshly loaded resource: {}", resource); + return resource; + } + } + + protected EncodingEnum encodingForPath(Path path) { + var extension = fileExtension(path); + return FILE_EXTENSIONS.inverse().get(extension); + } + + /** + * Writes a resource to the specified file path. + * + * @param The type of the FHIR resource. + * @param resource The resource to write. + * @param path The file path to write the resource to. + */ + protected void writeResource(T resource, Path path) { + try { + if (path.getParent() != null) { + path.getParent().toFile().mkdirs(); + } + + try (var stream = new FileOutputStream(path.toFile())) { + String result = parserForEncoding(fhirContext, encodingForPath(path)) + .setPrettyPrint(true) + .encodeResourceToString(resource); + stream.write(result.getBytes()); + resource.setUserData(SOURCE_PATH_TAG, path); + this.resourceCache.put(path, resource); + } + } catch (IOException | SecurityException e) { + throw new UnclassifiedServerFailureException(500, "Unable to write resource to path %s".formatted(path)); + } + } + + private String fileExtension(Path path) { + var name = path.getFileName().toString(); + var lastPeriod = name.lastIndexOf("."); + if (lastPeriod == -1) { + return null; + } + + return name.substring(lastPeriod + 1).toLowerCase(); + } + + // True if the file extension is one of the supported file extensions + private boolean acceptByFileExtension(Path path) { + var extension = fileExtension(path); + if (extension == null) { + return false; + } + + return FILE_EXTENSIONS.containsValue(extension); + } + + // True if the file extension is one of the supported file extensions + // and the file name starts with the given prefix (resource type name) + private boolean acceptByFileExtensionAndPrefix(Path path, String prefix) { + var extensionAccepted = this.acceptByFileExtension(path); + if (!extensionAccepted) { + return false; + } + + return path.getFileName().toString().toLowerCase().startsWith(prefix.toLowerCase() + "-"); + } + + /** + * Reads all resources of a given type from the directory. + * Directory structure depends on conventions: + * - Flat layout: resources are located in the root directory (e.g., + * "/path/to/ig/root/") + * - Directory for category: resources are in subdirectories (e.g., + * @param igRepositoryCompartment The compartment context to use + * @return Map of resource IDs to resources. + */ + protected Map readDirectoryForResourceType( + Class resourceClass, IgStandardRepositoryCompartment igRepositoryCompartment) { + var path = this.directoryForResource(resourceClass, igRepositoryCompartment); + if (!path.toFile().exists()) { + return Collections.emptyMap(); + } + + var resources = new ConcurrentHashMap(); + Predicate resourceFileFilter; + switch (this.conventions.filenameMode()) { + case ID_ONLY: + resourceFileFilter = this::acceptByFileExtension; + break; + case TYPE_AND_ID: + default: + resourceFileFilter = p -> this.acceptByFileExtensionAndPrefix(p, resourceClass.getSimpleName()); + break; + } + + try (var paths = Files.walk(path)) { + paths.filter(resourceFileFilter) + .parallel() + .map(this::cachedReadResource) + .filter(Objects::nonNull) + .forEach(r -> { + if (!r.fhirType().equals(resourceClass.getSimpleName())) { + return; + } + + T validatedResource = validateResource(resourceClass, r, r.getIdElement()); + resources.put(r.getIdElement().toUnqualifiedVersionless(), validatedResource); + }); + + } catch (IOException e) { + throw new UnclassifiedServerFailureException(500, "Unable to read resources from path: %s".formatted(path)); + } + + return resources; + } + + @Override + public FhirContext fhirContext() { + return this.fhirContext; + } + + /** + * Reads a resource from the repository. + * Locates files like: + * - ID_ONLY: "123.json" (in the appropriate directory based on layout) + * - TYPE_AND_ID: "Patient-123.json" + * Utilizes cache to improve performance. + * + *

+ * Example Usage: + *

+ * + *
{@code
+     * IIdType resourceId = new IdType("Patient", "12345");
+     * Map headers = new HashMap<>();
+     * Patient patient = repository.read(Patient.class, resourceId, headers);
+     * }
+ * + * @param The type of the FHIR resource. + * @param The type of the resource identifier. + * @param resourceType The class representing the FHIR resource type. + * @param id The identifier of the resource. + * @param headers Additional headers (not used in this implementation). + * @return The resource if found. + * @throws ResourceNotFoundException if the resource is not found. + */ + @Override + public T read( + Class resourceType, I id, Map headers) { + requireNonNull(resourceType, "resourceType cannot be null"); + requireNonNull(id, "id cannot be null"); + log.info("IgStandardRepository.read - Attempting to read resource [{}].", id); + + log.info("IgStandardRepository.read - headers: {}", headers); + var compartment = compartmentFrom(headers); + + var paths = this.potentialPathsForResource(resourceType, id, compartment); + for (var path : paths) { + log.info("IgStandardRepository.read - potentialPathsForResource path: {}", path); + if (!Files.exists(path)) { // if (!path.toFile().exists()) { + log.info("IgStandardRepository.read - File doesn't exist at [{}]. Continuing loop.", path); + continue; + } + + var resource = cachedReadResource(path); + if (resource != null) { + log.info("IgStandardRepository.read - Found resource [{}].", id); + return validateResource(resourceType, resource, id); + } + } + + log.info("IgStandardRepository.read - Unable to find resource [{}]. Throwing Exception", id); + throw new ResourceNotFoundException(id); + } + + /** + * Creates a new resource in the repository. + * + *

+ * Example Usage: + *

+ * + *
{@code
+     * Patient newPatient = new Patient();
+     * newPatient.setId("67890");
+     * newPatient.addName().setFamily("Doe").addGiven("John");
+     * Map headers = new HashMap<>();
+     * MethodOutcome outcome = repository.create(newPatient, headers);
+     * }
+ * + * @param The type of the FHIR resource. + * @param resource The resource to create. + * @param headers Additional headers (not used in this implementation). + * @return A {@link MethodOutcome} containing the outcome of the create + * operation. + */ + @Override + public MethodOutcome create(T resource, Map headers) { + requireNonNull(resource, "resource cannot be null"); + requireNonNull(resource.getIdElement().getIdPart(), "resource id cannot be null"); + + var compartment = compartmentFrom(headers); + + var path = this.preferredPathForResource(resource.getClass(), resource.getIdElement(), compartment); + writeResource(resource, path); + + return new MethodOutcome(resource.getIdElement(), true); + } + + private T validateResource(Class resourceType, IBaseResource resource, IIdType id) { + // All freshly read resources are tagged with their source path + var path = (Path) resource.getUserData(SOURCE_PATH_TAG); + + if (!resourceType.getSimpleName().equals(resource.fhirType())) { + throw new ResourceNotFoundException( + "Expected to find a resource with type: %s at path: %s. Found resource with type %s instead." + .formatted(resourceType.getSimpleName(), path, resource.fhirType())); + } + + if (!resource.getIdElement().hasIdPart()) { + throw new ResourceNotFoundException( + "Expected to find a resource with id: %s at path: %s. Found resource without an id instead." + .formatted(id.toUnqualifiedVersionless(), path)); + } + + if (!id.getIdPart().equals(resource.getIdElement().getIdPart())) { + throw new ResourceNotFoundException( + "Expected to find a resource with id: %s at path: %s. Found resource with an id %s instead." + .formatted( + id.getIdPart(), + path, + resource.getIdElement().getIdPart())); + } + + if (id.hasVersionIdPart() + && !id.getVersionIdPart().equals(resource.getIdElement().getVersionIdPart())) { + throw new ResourceNotFoundException( + "Expected to find a resource with version: %s at path: %s. Found resource with version %s instead." + .formatted( + id.getVersionIdPart(), + path, + resource.getIdElement().getVersionIdPart())); + } + + return resourceType.cast(resource); + } + + /** + * Updates an existing resource in the repository. + * + *

+ * Example Usage: + *

+ * + *
{@code
+     * Map headers = new HashMap<>();
+     * Patient existingPatient = repository.read(Patient.class, new IdType("Patient", "12345"), headers);
+     * existingPatient.addAddress().setCity("New City");
+     * MethodOutcome updateOutcome = repository.update(existingPatient, headers);
+     * }
+ * + * @param The type of the FHIR resource. + * @param resource The resource to update. + * @param headers Additional headers (not used in this implementation). + * @return A {@link MethodOutcome} containing the outcome of the update + * operation. + */ + @Override + public MethodOutcome update(T resource, Map headers) { + requireNonNull(resource, "resource cannot be null"); + requireNonNull(resource.getIdElement().getIdPart(), "resource id cannot be null"); + + var compartment = compartmentFrom(headers); + + var preferred = this.preferredPathForResource(resource.getClass(), resource.getIdElement(), compartment); + var actual = (Path) resource.getUserData(SOURCE_PATH_TAG); + if (actual == null) { + actual = preferred; + } + + if (isExternalPath(actual)) { + throw new ForbiddenOperationException( + "Unable to create or update: %s. Resource is marked as external, and external resources are read-only." + .formatted(resource.getIdElement().toUnqualifiedVersionless())); + } + + // If the preferred path and the actual path are different, and the encoding + // behavior is set to overwrite, + // move the resource to the preferred path and delete the old one. + if (!preferred.equals(actual) + && this.encodingBehavior.preserveEncoding() + == IgStandardEncodingBehavior.PreserveEncoding.OVERWRITE_WITH_PREFERRED_ENCODING) { + try { + Files.deleteIfExists(actual); + } catch (IOException e) { + throw new UnclassifiedServerFailureException(500, "Couldn't change encoding for %s".formatted(actual)); + } + + actual = preferred; + } + + writeResource(resource, actual); + + return new MethodOutcome(resource.getIdElement(), false); + } + + /** + * Deletes a resource from the repository. + * + *

+ * Example Usage: + *

+ * + *
{@code
+     * IIdType deleteId = new IdType("Patient", "67890");
+     * Map headers = new HashMap<>();
+     * MethodOutcome deleteOutcome = repository.delete(Patient.class, deleteId, headers);
+     * }
+ * + * @param The type of the FHIR resource. + * @param The type of the resource identifier. + * @param resourceType The class representing the FHIR resource type. + * @param id The identifier of the resource to delete. + * @param headers Additional headers (not used in this implementation). + * @return A {@link MethodOutcome} containing the outcome of the delete + * operation. + */ + @Override + public MethodOutcome delete( + Class resourceType, I id, Map headers) { + requireNonNull(resourceType, "resourceType cannot be null"); + requireNonNull(id, "id cannot be null"); + + var compartment = compartmentFrom(headers); + var paths = this.potentialPathsForResource(resourceType, id, compartment); + boolean deleted = false; + for (var path : paths) { + try { + deleted = Files.deleteIfExists(path); + if (deleted) { + break; + } + } catch (IOException e) { + throw new UnclassifiedServerFailureException(500, "Couldn't delete %s".formatted(path)); + } + } + + if (!deleted) { + throw new ResourceNotFoundException(id); + } + + return new MethodOutcome(id); + } + + /** + * Searches for resources matching the given search parameters. + * + *

+ * Example Usage: + *

+ * + *
{@code
+     * Map> searchParameters = new HashMap<>();
+     * searchParameters.put("family", Arrays.asList(new StringParam("Doe")));
+     * Map headers = new HashMap<>();
+     * IBaseBundle bundle = repository.search(Bundle.class, Patient.class, searchParameters, headers);
+     * }
+ * + * @param The type of the bundle to return. + * @param The type of the FHIR resource. + * @param bundleType The class representing the bundle type. + * @param resourceType The class representing the FHIR resource type. + * @param searchParameters The search parameters. + * @param headers Additional headers (not used in this implementation). + * @return A bundle containing the matching resources. + */ + @Override + @SuppressWarnings("unchecked") + public B search( + Class bundleType, + Class resourceType, + Multimap> searchParameters, + Map headers) { + BundleBuilder builder = new BundleBuilder(this.fhirContext); + builder.setType("searchset"); + + var compartment = compartmentFrom(headers); + + var resourceIdMap = readDirectoryForResourceType(resourceType, compartment); + if (searchParameters == null || searchParameters.isEmpty()) { + resourceIdMap.values().forEach(builder::addCollectionEntry); + return (B) builder.getBundle(); + } + + Collection candidates; + if (searchParameters.containsKey("_id")) { + // We are consuming the _id parameter in this if statement + candidates = getIdCandidates(searchParameters.get("_id"), resourceIdMap, resourceType); + searchParameters.removeAll("_id"); + } else { + candidates = resourceIdMap.values(); + } + + for (var resource : candidates) { + if (allParametersMatch(searchParameters, resource)) { + builder.addCollectionEntry(resource); + } + } + + return (B) builder.getBundle(); + } + + private List getIdCandidates( + Collection> idQueries, Map resourceIdMap, Class resourceType) { + var idResources = new ArrayList(); + for (var idQuery : idQueries) { + for (var query : idQuery) { + if (query instanceof TokenParam idToken) { + // Need to construct the equivalent "UnqualifiedVersionless" id that the map is + // indexed by. If an id has a version it won't match. Need apples-to-apples Id + // types + var id = Ids.newId(fhirContext, resourceType.getSimpleName(), idToken.getValue()); + var resource = resourceIdMap.get(id); + if (resource != null) { + idResources.add(resource); + } + } + } + } + return idResources; + } + + private boolean allParametersMatch( + Multimap> searchParameters, IBaseResource resource) { + for (var nextEntry : searchParameters.entries()) { + var paramName = nextEntry.getKey(); + if (!resourceMatcher.matches(paramName, nextEntry.getValue(), resource)) { + return false; + } + } + + return true; + } + + /** + * Invokes a FHIR operation on a resource type. + * + * @param The type of the resource returned by the operation. + * @param

The type of the parameters for the operation. + * @param The type of the resource on which the operation is + * invoked. + * @param resourceType The class representing the FHIR resource type. + * @param name The name of the operation. + * @param parameters The operation parameters. + * @param returnType The expected return type. + * @param headers Additional headers (not used in this implementation). + * @return The result of the operation. + */ + @Override + public R invoke( + Class resourceType, String name, P parameters, Class returnType, Map headers) { + return invokeOperation(null, resourceType.getSimpleName(), name, parameters); + } + + /** + * Invokes a FHIR operation on a specific resource instance. + * + * @param The type of the resource returned by the operation. + * @param

The type of the parameters for the operation. + * @param The type of the resource identifier. + * @param id The identifier of the resource. + * @param name The name of the operation. + * @param parameters The operation parameters. + * @param returnType The expected return type. + * @param headers Additional headers (not used in this implementation). + * @return The result of the operation. + */ + @Override + public R invoke( + I id, String name, P parameters, Class returnType, Map headers) { + return invokeOperation(id, id.getResourceType(), name, parameters); + } + + protected R invokeOperation( + IIdType id, String resourceType, String operationName, IBaseParameters parameters) { + if (operationProvider == null) { + throw new IllegalArgumentException("No operation provider found. Unable to invoke operations."); + } + return operationProvider.invokeOperation(this, id, resourceType, operationName, parameters); + } + + protected IgStandardRepositoryCompartment compartmentFrom(Map headers) { + if (headers == null) { + return new IgStandardRepositoryCompartment(); + } + + var compartmentHeader = headers.get(FHIR_COMPARTMENT_HEADER); + return compartmentHeader == null + ? new IgStandardRepositoryCompartment() + : new IgStandardRepositoryCompartment(compartmentHeader); + } + + protected String pathForCompartment(IgStandardRepositoryCompartment igStandardRepositoryCompartment) { + if (igStandardRepositoryCompartment.isEmpty()) { + return ""; + } + // The compartment path is typically ResourceType/Id (e.g., Patient/123) + // This is used as a directory name. + return igStandardRepositoryCompartment.getType() + "/" + igStandardRepositoryCompartment.getId(); + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.java new file mode 100644 index 00000000..d7875f18 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.java @@ -0,0 +1,84 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; +import java.util.StringJoiner; +import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; + +/** + * Class that represents the compartment context for a given request within {@link IgRepository} only. + */ +public class IgStandardRepositoryCompartment { + + private final String type; + private final String id; + + private static String typeOfContext(String context) { + return context.split("/")[0]; + } + + private static String idOfContext(String context) { + return context.split("/")[1]; + } + + // Empty context (i.e. no compartment context) + public IgStandardRepositoryCompartment() { + this.type = null; + this.id = null; + } + + // Context in the format ResourceType/Id + public IgStandardRepositoryCompartment(String context) { + this(typeOfContext(context), idOfContext(context)); + } + + // Context in the format type and id + public IgStandardRepositoryCompartment(String type, String id) { + // Make this lowercase so the path will resolve on Linux (FYI: macOS is case-insensitive) + this.type = requireNonNullOrEmpty("type", type).toLowerCase(); + this.id = requireNonNullOrEmpty("id", id); + } + + public String getType() { + return this.type; + } + + public String getId() { + return this.id; + } + + public boolean isEmpty() { + return this.type == null || this.id == null; + } + + private static String requireNonNullOrEmpty(String name, String value) { + requireNonNull(name, "name cannot be null"); + if (value == null || value.isEmpty()) { + throw new IllegalArgumentException(name + " cannot be null or empty"); + } + return value; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + IgStandardRepositoryCompartment that = (IgStandardRepositoryCompartment) o; + return Objects.equals(type, that.type) && Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(type, id); + } + + @Override + public String toString() { + return new StringJoiner(", ", IgStandardRepositoryCompartment.class.getSimpleName() + "[", "]") + .add("type='" + type + "'") + .add("id='" + id + "'") + .toString(); + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.java new file mode 100644 index 00000000..f7439fe4 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.java @@ -0,0 +1,24 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import com.google.common.collect.Sets; +import java.util.Set; + +enum IgStandardResourceCategory { + DATA, + TERMINOLOGY, + CONTENT; + + private static final Set TERMINOLOGY_RESOURCES = Sets.newHashSet("ValueSet", "CodeSystem"); + private static final Set CONTENT_RESOURCES = Sets.newHashSet( + "Library", "Questionnaire", "Measure", "PlanDefinition", "StructureDefinition", "ActivityDefinition"); + + static IgStandardResourceCategory forType(String resourceType) { + if (TERMINOLOGY_RESOURCES.contains(resourceType)) { + return TERMINOLOGY; + } else if (CONTENT_RESOURCES.contains(resourceType)) { + return CONTENT; + } + + return DATA; + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.java index 7beb41ef..5fbd0f5d 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.java @@ -1,10 +1,12 @@ package org.opencds.cqf.cql.ls.server.visitor; -import org.cqframework.cql.elm.tracking.TrackBack; +import org.cqframework.cql.cql2elm.tracking.TrackBack; +import org.cqframework.cql.cql2elm.tracking.Trackable; import org.cqframework.cql.elm.visiting.BaseElmLibraryVisitor; import org.hl7.elm.r1.Element; import org.hl7.elm.r1.ExpressionDef; import org.hl7.elm.r1.Retrieve; +import org.jetbrains.annotations.NotNull; public class ExpressionTrackBackVisitor extends BaseElmLibraryVisitor { @@ -35,7 +37,7 @@ public Element visitRetrieve(Retrieve retrieve, TrackBack context) { } protected boolean elementCoversTrackBack(Element elm, TrackBack context) { - for (TrackBack tb : elm.getTrackbacks()) { + for (TrackBack tb : Trackable.INSTANCE.getTrackbacks(elm)) { if (startsOnOrBefore(tb, context) && endsOnOrAfter(tb, context)) { return true; } @@ -69,4 +71,9 @@ protected boolean endsOnOrAfter(TrackBack left, TrackBack right) { // Same line return left.getEndChar() >= right.getEndChar(); } + + @Override + protected Element defaultResult(@NotNull Element element, TrackBack trackBack) { + return null; + } } diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.java new file mode 100644 index 00000000..612be057 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.java @@ -0,0 +1,31 @@ +package org.opencds.cqf.cql.ls.server.provider; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.util.Set; +import org.hl7.elm.r1.VersionedIdentifier; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.cql.ls.core.ContentService; +import org.opencds.cqf.cql.ls.core.utility.Uris; + +public class ContentServiceProviderTest { + + @Test + void should_throwException_when_gettingLibrary() throws Exception { + VersionedIdentifier versionedIdentifier = new VersionedIdentifier(); + versionedIdentifier.withVersion("1.0.0"); + + ContentServiceSourceProvider contentServiceSourceProvider = new ContentServiceSourceProvider( + Uris.parseOrNull("/provider/content/sample-library-1.0.0.json"), new ContentService() { + @Override + public Set locate(URI root, VersionedIdentifier identifier) { + throw new UncheckedIOException(new IOException()); + } + }); + + assertThrows(RuntimeException.class, () -> contentServiceSourceProvider.getLibrarySource(versionedIdentifier)); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/BadDataTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/BadDataTest.java new file mode 100644 index 00000000..fab426ac --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/BadDataTest.java @@ -0,0 +1,70 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.stream.Stream; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.search.Searches; + +class BadDataTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/badData", tempDir); + repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); + } + + @ParameterizedTest + @MethodSource("invalidContentTestData") + void readInvalidContentThrowsException(IIdType id, String errorMessage) { + var e = assertThrows(ResourceNotFoundException.class, () -> repository.read(Patient.class, id)); + assertTrue(e.getMessage().contains(errorMessage)); + } + + @Test + void nonFhirFilesAreIgnored() { + var id = new IdType("Patient/NotAFhirFile"); + assertThrows(ResourceNotFoundException.class, () -> repository.read(Patient.class, id)); + } + + @Test + void searchThrowsBecauseOfInvalidContent() { + // If there's any invalid content in the directory, the search will fail + assertThrows( + ResourceNotFoundException.class, () -> repository.search(Bundle.class, Patient.class, Searches.ALL)); + } + + private static Stream invalidContentTestData() { + return Stream.of( + Arguments.of(new IdType("Patient/InvalidContent"), "Found empty or invalid content"), + Arguments.of(new IdType("Patient/MissingId"), "Found resource without an id"), + Arguments.of(new IdType("Patient/NoContent"), "Found empty or invalid content"), + Arguments.of(new IdType("Patient/WrongId"), "Found resource with an id DoesntMatchFilename"), + Arguments.of(new IdType("Patient/WrongResourceType"), "Found resource with type Encounter"), + Arguments.of(new IdType("Patient/WrongVersion").withVersion("1"), "Found resource with version 2")); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CompartmentTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CompartmentTest.java new file mode 100644 index 00000000..7379058a --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CompartmentTest.java @@ -0,0 +1,243 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.search.Searches; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class CompartmentTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/compartment", tempDir); + repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); + } + + @Test + void readLibrary() { + var id = Ids.newId(Library.class, "123"); + var lib = repository.read(Library.class, id); + assertNotNull(lib); + assertEquals(id.getIdPart(), lib.getIdElement().getIdPart()); + } + + @Test + void readLibraryNotExists() { + var id = Ids.newId(Library.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); + } + + @Test + void searchLibrary() { + var libs = repository.search(Bundle.class, Library.class, Searches.ALL); + + assertNotNull(libs); + assertEquals(2, libs.getEntry().size()); + } + + @Test + void searchLibraryNotExists() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("not-exists")); + assertNotNull(libs); + assertEquals(0, libs.getEntry().size()); + } + + @Test + void readPatientNoCompartment() { + var id = Ids.newId(Patient.class, "123"); + assertThrows(ResourceNotFoundException.class, () -> repository.read(Patient.class, id)); + } + + @Test + void readPatient() { + var id = Ids.newId(Patient.class, "123"); + var p = repository.read(Patient.class, id, Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/123")); + + assertNotNull(p); + assertEquals(id.getIdPart(), p.getIdElement().getIdPart()); + } + + @Test + void searchEncounterNoCompartment() { + var encounters = repository.search(Bundle.class, Encounter.class, Searches.ALL); + assertNotNull(encounters); + assertEquals(0, encounters.getEntry().size()); + } + + @Test + void searchEncounter() { + var encounters = repository.search( + Bundle.class, + Encounter.class, + Searches.ALL, + Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/123")); + assertNotNull(encounters); + assertEquals(1, encounters.getEntry().size()); + } + + @Test + void readValueSetNoCompartment() { + var id = Ids.newId(ValueSet.class, "456"); + var vs = repository.read(ValueSet.class, id); + + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + } + + // Terminology resources are not in compartments + @Test + void readValueSet() { + var id = Ids.newId(ValueSet.class, "456"); + var vs = repository.read( + ValueSet.class, id, Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/123")); + + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + } + + @Test + void searchValueSet() { + var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/456")); + assertNotNull(sets); + assertEquals(1, sets.getEntry().size()); + } + + @Test + void createAndDeleteLibrary() { + var lib = new Library(); + lib.setId("new-library"); + var o = repository.create(lib); + var created = repository.read(Library.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("resources/library/new-library.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Library.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeletePatient() { + var p = new Patient(); + p.setId("new-patient"); + var header = Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/new-patient"); + var o = repository.create(p, header); + var created = repository.read(Patient.class, o.getId(), header); + assertNotNull(created); + + var loc = tempDir.resolve("tests/patient/new-patient/patient/new-patient.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Patient.class, created.getIdElement(), header); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeleteValueSet() { + var v = new ValueSet(); + v.setId("new-valueset"); + var o = repository.create(v); + var created = repository.read(ValueSet.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("vocabulary/valueset/new-valueset.json"); + assertTrue(Files.exists(loc)); + + repository.delete(ValueSet.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void updatePatient() { + var id = Ids.newId(Patient.class, "123"); + var p = repository.read(Patient.class, id, Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/123")); + assertFalse(p.hasActive()); + + p.setActive(true); + repository.update(p); + + var updated = + repository.read(Patient.class, id, Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/123")); + assertTrue(updated.hasActive()); + assertTrue(updated.getActive()); + } + + @Test + void deleteNonExistentPatient() { + var id = Ids.newId(Patient.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.delete(Patient.class, id)); + } + + @Test + void searchNonExistentType() { + var results = repository.search(Bundle.class, Encounter.class, Searches.ALL); + assertNotNull(results); + assertEquals(0, results.getEntry().size()); + } + + @Test + void searchById() { + var bundle = repository.search(Bundle.class, Library.class, Searches.byId("123")); + assertNotNull(bundle); + assertEquals(1, bundle.getEntry().size()); + } + + @Test + void searchByIdNotFound() { + var bundle = repository.search(Bundle.class, Library.class, Searches.byId("DoesNotExist")); + assertNotNull(bundle); + assertEquals(0, bundle.getEntry().size()); + } + + @Test + @Order(1) // Do this test first because it puts the filesystem (temporarily) in an invalid state + void resourceMissingWhenCacheCleared() throws IOException { + var id = new IdType("Library", "ToDelete"); + var lib = new Library().setIdElement(id); + var path = tempDir.resolve("resources/library/ToDelete.json"); + + repository.create(lib); + assertTrue(path.toFile().exists()); + + // Read back, should exist + lib = repository.read(Library.class, id); + assertNotNull(lib); + + // Overwrite the file on disk. + Files.writeString(path, ""); + + // Read from cache, repo doesn't know the content is gone. + lib = repository.read(Library.class, id); + assertNotNull(lib); + assertEquals("ToDelete", lib.getIdElement().getIdPart()); + + ((IgStandardRepository) repository).clearCache(); + + // Try to read again, should be gone because it's not in the cache and the content is gone. + assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); + + // Clean up so that we don't affect other tests + path.toFile().delete(); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.java new file mode 100644 index 00000000..aa6e14b3 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.java @@ -0,0 +1,88 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; + +class ConventionsTest { + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard", tempDir); + } + + @Test + void autoDetectDefault() { + assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(null)); + assertEquals( + IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("does_not_exist"))); + } + + @Test + void autoDetectStandard() { + assertEquals( + IgStandardConventions.STANDARD, + IgStandardConventions.autoDetect(tempDir.resolve("directoryPerType/standard"))); + } + + @Test + void autoDetectPrefix() { + var config = IgStandardConventions.autoDetect(tempDir.resolve("directoryPerType/prefixed")); + assertEquals(IgStandardConventions.FilenameMode.TYPE_AND_ID, config.filenameMode()); + assertEquals(IgStandardConventions.CategoryLayout.DIRECTORY_PER_CATEGORY, config.categoryLayout()); + assertEquals(IgStandardConventions.CompartmentLayout.FLAT, config.compartmentLayout()); + assertEquals(IgStandardConventions.FhirTypeLayout.DIRECTORY_PER_TYPE, config.typeLayout()); + } + + @Test + void autoDetectFlat() { + assertEquals(IgStandardConventions.FLAT, IgStandardConventions.autoDetect(tempDir.resolve("flat"))); + } + + @Test + void autoDetectFlatNoTypeNames() { + var config = IgStandardConventions.autoDetect(tempDir.resolve("flatNoTypeNames")); + assertEquals(IgStandardConventions.FilenameMode.ID_ONLY, config.filenameMode()); + assertEquals(IgStandardConventions.CategoryLayout.FLAT, config.categoryLayout()); + assertEquals(IgStandardConventions.CompartmentLayout.FLAT, config.compartmentLayout()); + assertEquals(IgStandardConventions.FhirTypeLayout.FLAT, config.typeLayout()); + } + + @Test + void autoDetectWithMisleadingFileName() { + assertEquals( + IgStandardConventions.STANDARD, + IgStandardConventions.autoDetect(tempDir.resolve("misleadingFileName"))); + } + + @Test + void autoDetectWithEmptyContent() { + assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("emptyContent"))); + } + + @Test + void autoDetectWithNonFhirFilename() { + assertEquals( + IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("nonFhirFilename"))); + } + + @Test + void autoDetectWitCompartments() { + var config = IgStandardConventions.autoDetect(tempDir.resolve("compartment")); + assertEquals(IgStandardConventions.FilenameMode.ID_ONLY, config.filenameMode()); + assertEquals(IgStandardConventions.CategoryLayout.DIRECTORY_PER_CATEGORY, config.categoryLayout()); + assertEquals(IgStandardConventions.CompartmentLayout.DIRECTORY_PER_COMPARTMENT, config.compartmentLayout()); + assertEquals(IgStandardConventions.FhirTypeLayout.DIRECTORY_PER_TYPE, config.typeLayout()); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.java new file mode 100644 index 00000000..004af4a6 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.java @@ -0,0 +1,84 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import org.hl7.fhir.dstu2.model.ValueSet; +import org.hl7.fhir.dstu3.model.Library; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; + +class CqlContentTest { + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws ClassNotFoundException, URISyntaxException, IOException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/cqlContent", tempDir); + } + + @Test + void loadCqlContentDstu3() { + var lib = new Library(); + lib.addContent().setContentType("text/cql").setUrl("cql/Test.cql"); + IgStandardCqlContent.loadCqlContent(lib, tempDir); + assertNotNull(lib.getContentFirstRep().getData()); + } + + @Test + void loadCqlContentR4() { + var lib = new org.hl7.fhir.r4.model.Library(); + lib.addContent().setContentType("text/cql").setUrl("cql/Test.cql"); + IgStandardCqlContent.loadCqlContent(lib, tempDir); + assertNotNull(lib.getContentFirstRep().getData()); + } + + @Test + void loadCqlContentR5() { + var lib = new org.hl7.fhir.r5.model.Library(); + lib.addContent().setContentType("text/cql").setUrl("cql/Test.cql"); + IgStandardCqlContent.loadCqlContent(lib, tempDir); + assertNotNull(lib.getContentFirstRep().getData()); + } + + @Test + void emptyLibraryDoesNothing() { + var lib = new Library(); + IgStandardCqlContent.loadCqlContent(lib, tempDir); + assertEquals(0, lib.getContent().size()); + } + + @Test + void nonLibraryResourceDoesNotThrow() { + assertDoesNotThrow(() -> IgStandardCqlContent.loadCqlContent(new ValueSet(), tempDir)); + } + + @Test + void invalidFhirVersionThrows() { + var lib = new org.hl7.fhir.r4b.model.Library(); + assertThrows(IllegalArgumentException.class, () -> IgStandardCqlContent.loadCqlContent(lib, tempDir)); + } + + @Test + void invalidPathThrows() { + var lib = new org.hl7.fhir.r4.model.Library(); + lib.addContent().setContentType("text/cql").setUrl("not-a-real-path/Test.cql"); + assertThrows(ResourceNotFoundException.class, () -> IgStandardCqlContent.loadCqlContent(lib, tempDir)); + } + + @Test + void nullThrows() { + assertThrows(NullPointerException.class, () -> IgStandardCqlContent.loadCqlContent(null, tempDir)); + + var lib = new Library(); + assertThrows(NullPointerException.class, () -> IgStandardCqlContent.loadCqlContent(lib, null)); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.java new file mode 100644 index 00000000..443d0ba1 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.java @@ -0,0 +1,176 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.search.Searches; + +class DirectoryTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/directoryPerType/standard", tempDir); + repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); + } + + @Test + void readLibrary() { + var id = Ids.newId(Library.class, "123"); + var lib = repository.read(Library.class, id); + assertNotNull(lib); + assertEquals(id.getIdPart(), lib.getIdElement().getIdPart()); + } + + @Test + void readLibraryNotExists() { + var id = Ids.newId(Library.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); + } + + @Test + void searchLibrary() { + var libs = repository.search(Bundle.class, Library.class, Searches.ALL); + + assertNotNull(libs); + assertEquals(2, libs.getEntry().size()); + } + + @Test + void searchLibraryWithFilter() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("http://example.com/Library/Test")); + + assertNotNull(libs); + assertEquals(1, libs.getEntry().size()); + } + + @Test + void searchLibraryNotExists() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("not-exists")); + assertNotNull(libs); + assertEquals(0, libs.getEntry().size()); + } + + @Test + void readPatient() { + var id = Ids.newId(Patient.class, "ABC"); + var cond = repository.read(Patient.class, id); + + assertNotNull(cond); + assertEquals(id.getIdPart(), cond.getIdElement().getIdPart()); + } + + @Test + void searchCondition() { + var cons = repository.search( + Bundle.class, Condition.class, Searches.byCodeAndSystem("12345", "example.com/codesystem")); + assertNotNull(cons); + assertEquals(2, cons.getEntry().size()); + } + + @Test + void readValueSet() { + var id = Ids.newId(ValueSet.class, "456"); + var vs = repository.read(ValueSet.class, id); + + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + } + + @Test + void searchValueSet() { + var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/456")); + assertNotNull(sets); + assertEquals(1, sets.getEntry().size()); + } + + @Test + void createAndDeleteLibrary() { + var lib = new Library(); + lib.setId("new-library"); + var o = repository.create(lib); + var created = repository.read(Library.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("resources/library/new-library.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Library.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeletePatient() { + var p = new Patient(); + p.setId("new-patient"); + var o = repository.create(p); + var created = repository.read(Patient.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("tests/patient/new-patient.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Patient.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeleteValueSet() { + var v = new ValueSet(); + v.setId("new-valueset"); + var o = repository.create(v); + var created = repository.read(ValueSet.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("vocabulary/valueset/new-valueset.json"); + assertTrue(Files.exists(loc)); + + repository.delete(ValueSet.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void updatePatient() { + var id = Ids.newId(Patient.class, "ABC"); + var p = repository.read(Patient.class, id); + assertFalse(p.hasActive()); + + p.setActive(true); + repository.update(p); + + var updated = repository.read(Patient.class, id); + assertTrue(updated.hasActive()); + assertTrue(updated.getActive()); + } + + @Test + void deleteNonExistentPatient() { + var id = Ids.newId(Patient.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.delete(Patient.class, id)); + } + + @Test + void searchNonExistentType() { + var results = repository.search(Bundle.class, Encounter.class, Searches.ALL); + assertNotNull(results); + assertEquals(0, results.getEntry().size()); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ExternalTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ExternalTest.java new file mode 100644 index 00000000..39d9da8a --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ExternalTest.java @@ -0,0 +1,87 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.search.Searches; + +class ExternalTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/externalResource", tempDir); + repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); + } + + @Test + void readValueSet() { + var id = Ids.newId(ValueSet.class, "456"); + var vs = repository.read(ValueSet.class, id); + + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + } + + @Test + void createAndDeleteValueSet() { + var v = new ValueSet(); + v.setId("new-valueset"); + var o = repository.create(v); + var created = repository.read(ValueSet.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("vocabulary/valueset/new-valueset.json"); + assertTrue(Files.exists(loc)); + + repository.delete(ValueSet.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void readExternalValueSet() { + var id = Ids.newId(ValueSet.class, "789"); + var vs = repository.read(ValueSet.class, id); + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + + // Should be tagged with its source path + var path = (Path) vs.getUserData(IgStandardRepository.SOURCE_PATH_TAG); + assertNotNull(path); + assertTrue(path.toFile().exists()); + assertTrue(path.toString().contains("external")); + } + + @Test + void searchExternalValueSet() { + var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/789")); + assertNotNull(sets); + assertEquals(1, sets.getEntry().size()); + } + + @Test + void updateExternalValueSetFails() { + var id = Ids.newId(ValueSet.class, "789"); + var vs = repository.read(ValueSet.class, id); + assertThrows(ForbiddenOperationException.class, () -> repository.update(vs)); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatNoTypeNamesTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatNoTypeNamesTest.java new file mode 100644 index 00000000..42c5289e --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatNoTypeNamesTest.java @@ -0,0 +1,176 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.search.Searches; + +class FlatNoTypeNamesTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/flatNoTypeNames", tempDir); + repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); + } + + @Test + void readLibrary() { + var id = Ids.newId(Library.class, "123"); + var lib = repository.read(Library.class, id); + assertNotNull(lib); + assertEquals(id.getIdPart(), lib.getIdElement().getIdPart()); + } + + @Test + void readLibraryNotExists() { + var id = Ids.newId(Library.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); + } + + @Test + void searchLibrary() { + var libs = repository.search(Bundle.class, Library.class, Searches.ALL); + + assertNotNull(libs); + assertEquals(2, libs.getEntry().size()); + } + + @Test + void searchLibraryWithFilter() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("http://example.com/Library/Test")); + + assertNotNull(libs); + assertEquals(1, libs.getEntry().size()); + } + + @Test + void searchLibraryNotExists() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("not-exists")); + assertNotNull(libs); + assertEquals(0, libs.getEntry().size()); + } + + @Test + void readPatient() { + var id = Ids.newId(Patient.class, "ABC"); + var cond = repository.read(Patient.class, id); + + assertNotNull(cond); + assertEquals(id.getIdPart(), cond.getIdElement().getIdPart()); + } + + @Test + void searchCondition() { + var cons = repository.search( + Bundle.class, Condition.class, Searches.byCodeAndSystem("12345", "example.com/codesystem")); + assertNotNull(cons); + assertEquals(2, cons.getEntry().size()); + } + + @Test + void readValueSet() { + var id = Ids.newId(ValueSet.class, "789"); + var vs = repository.read(ValueSet.class, id); + + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + } + + @Test + void searchValueSet() { + var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/789")); + assertNotNull(sets); + assertEquals(1, sets.getEntry().size()); + } + + @Test + void createAndDeleteLibrary() { + var lib = new Library(); + lib.setId("new-library"); + var o = repository.create(lib); + var created = repository.read(Library.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("new-library.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Library.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeletePatient() { + var p = new Patient(); + p.setId("new-patient"); + var o = repository.create(p); + var created = repository.read(Patient.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("new-patient.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Patient.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeleteValueSet() { + var v = new ValueSet(); + v.setId("new-valueset"); + var o = repository.create(v); + var created = repository.read(ValueSet.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("new-valueset.json"); + assertTrue(Files.exists(loc)); + + repository.delete(ValueSet.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void updatePatient() { + var id = Ids.newId(Patient.class, "ABC"); + var p = repository.read(Patient.class, id); + assertFalse(p.hasActive()); + + p.setActive(true); + repository.update(p); + + var updated = repository.read(Patient.class, id); + assertTrue(updated.hasActive()); + assertTrue(updated.getActive()); + } + + @Test + void deleteNonExistentPatient() { + var id = Ids.newId(Patient.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.delete(Patient.class, id)); + } + + @Test + void searchNonExistentType() { + var results = repository.search(Bundle.class, Encounter.class, Searches.ALL); + assertNotNull(results); + assertEquals(0, results.getEntry().size()); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatTest.java new file mode 100644 index 00000000..eb7d0b85 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatTest.java @@ -0,0 +1,176 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.search.Searches; + +class FlatTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/flat", tempDir); + repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); + } + + @Test + void readLibrary() { + var id = Ids.newId(Library.class, "123"); + var lib = repository.read(Library.class, id); + assertNotNull(lib); + assertEquals(id.getIdPart(), lib.getIdElement().getIdPart()); + } + + @Test + void readLibraryNotExists() { + var id = Ids.newId(Library.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); + } + + @Test + void searchLibrary() { + var libs = repository.search(Bundle.class, Library.class, Searches.ALL); + + assertNotNull(libs); + assertEquals(2, libs.getEntry().size()); + } + + @Test + void searchLibraryWithFilter() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("http://example.com/Library/Test")); + + assertNotNull(libs); + assertEquals(1, libs.getEntry().size()); + } + + @Test + void searchLibraryNotExists() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("not-exists")); + assertNotNull(libs); + assertEquals(0, libs.getEntry().size()); + } + + @Test + void readPatient() { + var id = Ids.newId(Patient.class, "ABC"); + var cond = repository.read(Patient.class, id); + + assertNotNull(cond); + assertEquals(id.getIdPart(), cond.getIdElement().getIdPart()); + } + + @Test + void searchCondition() { + var cons = repository.search( + Bundle.class, Condition.class, Searches.byCodeAndSystem("12345", "example.com/codesystem")); + assertNotNull(cons); + assertEquals(2, cons.getEntry().size()); + } + + @Test + void readValueSet() { + var id = Ids.newId(ValueSet.class, "456"); + var vs = repository.read(ValueSet.class, id); + + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + } + + @Test + void searchValueSet() { + var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/456")); + assertNotNull(sets); + assertEquals(1, sets.getEntry().size()); + } + + @Test + void createAndDeleteLibrary() { + var lib = new Library(); + lib.setId("new-library"); + var o = repository.create(lib); + var created = repository.read(Library.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("Library-new-library.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Library.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeletePatient() { + var p = new Patient(); + p.setId("new-patient"); + var o = repository.create(p); + var created = repository.read(Patient.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("Patient-new-patient.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Patient.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeleteValueSet() { + var v = new ValueSet(); + v.setId("new-valueset"); + var o = repository.create(v); + var created = repository.read(ValueSet.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("ValueSet-new-valueset.json"); + assertTrue(Files.exists(loc)); + + repository.delete(ValueSet.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void updatePatient() { + var id = Ids.newId(Patient.class, "ABC"); + var p = repository.read(Patient.class, id); + assertFalse(p.hasActive()); + + p.setActive(true); + repository.update(p); + + var updated = repository.read(Patient.class, id); + assertTrue(updated.hasActive()); + assertTrue(updated.getActive()); + } + + @Test + void deleteNonExistentPatient() { + var id = Ids.newId(Patient.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.delete(Patient.class, id)); + } + + @Test + void searchNonExistentType() { + var results = repository.search(Bundle.class, Encounter.class, Searches.ALL); + assertNotNull(results); + assertEquals(0, results.getEntry().size()); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MixedEncodingTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MixedEncodingTest.java new file mode 100644 index 00000000..fd6be6d5 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MixedEncodingTest.java @@ -0,0 +1,181 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.search.Searches; + +class MixedEncodingTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/mixedEncoding", tempDir); + repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); + } + + @Test + void readLibrary() { + var id = Ids.newId(Library.class, "123"); + var lib = repository.read(Library.class, id); + assertNotNull(lib); + assertEquals(id.getIdPart(), lib.getIdElement().getIdPart()); + } + + @Test + void readLibraryNotExists() { + var id = Ids.newId(Library.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); + } + + @Test + void searchLibrary() { + var libs = repository.search(Bundle.class, Library.class, Searches.ALL); + + assertNotNull(libs); + assertEquals(1, libs.getEntry().size()); + } + + @Test + void searchLibraryWithFilter() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("http://example.com/Library/123")); + + assertNotNull(libs); + assertEquals(1, libs.getEntry().size()); + } + + @Test + void searchLibraryNotExists() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("not-exists")); + assertNotNull(libs); + assertEquals(0, libs.getEntry().size()); + } + + @Test + void readValueSet() { + var id = Ids.newId(ValueSet.class, "456"); + var vs = repository.read(ValueSet.class, id); + + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + } + + @Test + void searchValueSet() { + var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/456")); + assertNotNull(sets); + assertEquals(1, sets.getEntry().size()); + } + + @Test + void searchWithExternalValueSet() { + var sets = repository.search(Bundle.class, ValueSet.class, Searches.ALL); + assertNotNull(sets); + assertEquals(2, sets.getEntry().size()); + } + + @Test + void createAndDeleteLibrary() { + var lib = new Library(); + lib.setId("new-library"); + var o = repository.create(lib); + var created = repository.read(Library.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("resources/library/new-library.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Library.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeletePatient() { + var p = new Patient(); + p.setId("new-patient"); + var o = repository.create(p); + var created = repository.read(Patient.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("tests/patient/new-patient.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Patient.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeleteValueSet() { + var v = new ValueSet(); + v.setId("new-valueset"); + var o = repository.create(v); + var created = repository.read(ValueSet.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("vocabulary/valueset/new-valueset.json"); + assertTrue(Files.exists(loc)); + + repository.delete(ValueSet.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void deleteNonExistentPatient() { + var id = Ids.newId(Patient.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.delete(Patient.class, id)); + } + + @Test + void searchNonExistentType() { + var results = repository.search(Bundle.class, Encounter.class, Searches.ALL); + assertNotNull(results); + assertEquals(0, results.getEntry().size()); + } + + @Test + void readExternalValueSet() { + var id = Ids.newId(ValueSet.class, "789"); + var vs = repository.read(ValueSet.class, id); + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + + // Should be tagged with its source path + var path = (Path) vs.getUserData(IgStandardRepository.SOURCE_PATH_TAG); + assertNotNull(path); + assertTrue(path.toFile().exists()); + assertTrue(path.toString().contains("external")); + } + + @Test + void searchExternalValueSet() { + var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/789")); + assertNotNull(sets); + assertEquals(1, sets.getEntry().size()); + } + + @Test + void updateExternalValueSetFails() { + var id = Ids.newId(ValueSet.class, "789"); + var vs = repository.read(ValueSet.class, id); + assertThrows(ForbiddenOperationException.class, () -> repository.update(vs)); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.java new file mode 100644 index 00000000..e1c5fdc7 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.java @@ -0,0 +1,117 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MultiMeasureTest { + private static final Logger log = LoggerFactory.getLogger(MultiMeasureTest.class); + + private static final String rootDir = "/sampleIgs/ig/standard/cqlMeasures/multiMeasure"; + private static final String modelPathMeasure100TestCase1111 = "input/tests/measure/measure100/1111"; + private static final String modelPathMeasure100TestCase2222 = "input/tests/measure/measure100/2222"; + private static final String modelPathMeasure200TestCase1111 = "input/tests/measure/measure200/1111"; + private static final String terminologyPath = "input/vocabulary/valueset"; + + @TempDir + static Path tempDir; + + @TempDir + static Path pathModelPathMeasure100TestCase1111; + + @TempDir + static Path pathModelPathMeasure100TestCase2222; + + @TempDir + static Path pathModelPathMeasure200TestCase1111; + + @TempDir + static Path pathTerminology; + + static IRepository model1111Measure100Repo; + static IRepository model2222Measure100Repo; + static IRepository model1111Measure200Repo; + static IRepository terminologyRepo; + + static void listFiles(Path path) { + var pathExists = path.toFile().exists(); + log.info("path[{}] exists: {}", path, pathExists); + if (pathExists) + try (Stream stream = Files.walk(path)) { + String fileNames = stream.map(Path::toString).collect(Collectors.joining("\n ")); + + log.info("resources: \n {}", fileNames); + } catch (IOException e) { + log.error("Exception while capturing filenames. {}", e.getMessage()); + } + } + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar(rootDir, tempDir); + + pathModelPathMeasure100TestCase1111 = tempDir.resolve(modelPathMeasure100TestCase1111); + pathModelPathMeasure100TestCase2222 = tempDir.resolve(modelPathMeasure100TestCase2222); + pathModelPathMeasure200TestCase1111 = tempDir.resolve(modelPathMeasure200TestCase1111); + pathTerminology = tempDir.resolve(terminologyPath); + + model1111Measure100Repo = + new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure100TestCase1111); + model2222Measure100Repo = + new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure100TestCase2222); + model1111Measure200Repo = + new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure200TestCase1111); + terminologyRepo = new IgStandardRepository(FhirContext.forR4Cached(), pathTerminology); + + listFiles(tempDir); + listFiles(pathModelPathMeasure100TestCase1111); + listFiles(pathModelPathMeasure100TestCase2222); + listFiles(pathModelPathMeasure200TestCase1111); + listFiles(pathTerminology); + } + + @Test + void should_throwException_when_libraryDoesNotExist() { + var id = Ids.newId(Library.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> model1111Measure100Repo.read(Library.class, id)); + assertThrows(ResourceNotFoundException.class, () -> model2222Measure100Repo.read(Library.class, id)); + assertThrows(ResourceNotFoundException.class, () -> model1111Measure200Repo.read(Library.class, id)); + assertThrows(ResourceNotFoundException.class, () -> terminologyRepo.read(Library.class, id)); + } + + // Test works locally but doesn't work on GitHub + // @Disabled("Disabled until issue with running test on github is resolved.") + @Test + void should_findResourceInCorrectRepo_when_resourcesIsolatedByRepo() { + var id = Ids.newId(Patient.class, "1111"); + var patientFrommModel1111Measure100Repo = model1111Measure100Repo.read(Patient.class, id); + var patientFrommModel1111Measure200Repo = model1111Measure200Repo.read(Patient.class, id); + + assertEquals( + id.getIdPart(), + patientFrommModel1111Measure100Repo.getIdElement().getIdPart()); + assertEquals( + id.getIdPart(), + patientFrommModel1111Measure200Repo.getIdElement().getIdPart()); + assertNotEquals(patientFrommModel1111Measure100Repo.getName(), patientFrommModel1111Measure200Repo.getName()); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.java new file mode 100644 index 00000000..39e4e96d --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.java @@ -0,0 +1,141 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.search.Searches; + +/** + * This set of tests ensures that we can create new directories as needed if + * they don't exist ahead of time + */ +class NoTestDataTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/noTestData", tempDir); + repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); + } + + @Test + void createAndDeleteLibrary() { + var lib = new Library(); + lib.setId("new-library"); + var o = repository.create(lib); + var created = repository.read(Library.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("resources/library/new-library.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Library.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeleteMeasure() { + var measure = new Measure(); + measure.setId("new-measure"); + var o = repository.create(measure); + var created = repository.read(Measure.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("resources/measure/new-measure.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Measure.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeletePatient() { + var p = new Patient(); + p.setId("new-patient"); + var o = repository.create(p); + var created = repository.read(Patient.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("tests/patient/new-patient.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Patient.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeleteCondition() { + var p = new Condition(); + p.setId("new-condition"); + var o = repository.create(p); + var created = repository.read(Condition.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("tests/condition/new-condition.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Condition.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeleteValueSet() { + var v = new ValueSet(); + v.setId("new-valueset"); + var o = repository.create(v); + var created = repository.read(ValueSet.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("vocabulary/valueset/new-valueset.json"); + assertTrue(Files.exists(loc)); + + repository.delete(ValueSet.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeleteCodeSystem() { + var c = new CodeSystem(); + c.setId("new-codesystem"); + var o = repository.create(c); + var created = repository.read(CodeSystem.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("vocabulary/codesystem/new-codesystem.json"); + assertTrue(Files.exists(loc)); + + repository.delete(CodeSystem.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void deleteNonExistentPatient() { + var id = Ids.newId(Patient.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.delete(Patient.class, id)); + } + + @Test + void searchNonExistentType() { + var results = repository.search(Bundle.class, Encounter.class, Searches.ALL); + assertNotNull(results); + assertEquals(0, results.getEntry().size()); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/OverwriteEncodingTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/OverwriteEncodingTest.java new file mode 100644 index 00000000..a432871f --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/OverwriteEncodingTest.java @@ -0,0 +1,62 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.api.EncodingEnum; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import org.hl7.fhir.r4.model.Library; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; + +class OverwriteEncodingTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/mixedEncoding", tempDir); + var conventions = IgStandardConventions.autoDetect(tempDir); + repository = new IgStandardRepository( + FhirContext.forR4Cached(), + tempDir, + conventions, + new IgStandardEncodingBehavior( + EncodingEnum.XML, + IgStandardEncodingBehavior.PreserveEncoding.OVERWRITE_WITH_PREFERRED_ENCODING), + null); + } + + @Test + void readLibrary() { + var id = Ids.newId(Library.class, "123"); + var lib = repository.read(Library.class, id); + assertNotNull(lib); + assertEquals(id.getIdPart(), lib.getIdElement().getIdPart()); + } + + @Test + void updateLibrary() { + var id = Ids.newId(Library.class, "123"); + var lib = repository.read(Library.class, id); + assertNotNull(lib); + assertEquals(id.getIdPart(), lib.getIdElement().getIdPart()); + + lib.addAuthor().setName("Test Author"); + + repository.update(lib); + assertFalse(tempDir.resolve("resources/library/123.json").toFile().exists()); + assertTrue(tempDir.resolve("resources/library/123.xml").toFile().exists()); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/PrefixTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/PrefixTest.java new file mode 100644 index 00000000..a2f6d9b2 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/PrefixTest.java @@ -0,0 +1,221 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.hl7.fhir.r4.model.*; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.search.Searches; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class PrefixTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/directoryPerType/prefixed", tempDir); + repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); + } + + @Test + void readLibrary() { + var id = Ids.newId(Library.class, "123"); + var lib = repository.read(Library.class, id); + assertNotNull(lib); + assertEquals(id.getIdPart(), lib.getIdElement().getIdPart()); + } + + @Test + void readLibraryNotExists() { + var id = Ids.newId(Library.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); + } + + @Test + void searchLibrary() { + var libs = repository.search(Bundle.class, Library.class, Searches.ALL); + + assertNotNull(libs); + assertEquals(2, libs.getEntry().size()); + } + + @Test + void searchLibraryWithFilter() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("http://example.com/Library/Test")); + + assertNotNull(libs); + assertEquals(1, libs.getEntry().size()); + } + + @Test + void searchLibraryNotExists() { + var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("not-exists")); + assertNotNull(libs); + assertEquals(0, libs.getEntry().size()); + } + + @Test + void readPatient() { + var id = Ids.newId(Patient.class, "ABC"); + var cond = repository.read(Patient.class, id); + + assertNotNull(cond); + assertEquals(id.getIdPart(), cond.getIdElement().getIdPart()); + } + + @Test + void searchCondition() { + var cons = repository.search( + Bundle.class, Condition.class, Searches.byCodeAndSystem("12345", "example.com/codesystem")); + assertNotNull(cons); + assertEquals(2, cons.getEntry().size()); + } + + @Test + void readValueSet() { + var id = Ids.newId(ValueSet.class, "456"); + var vs = repository.read(ValueSet.class, id); + + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + } + + @Test + void searchValueSet() { + var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/456")); + assertNotNull(sets); + assertEquals(1, sets.getEntry().size()); + } + + @Test + void createAndDeleteLibrary() { + var lib = new Library(); + lib.setId("new-library"); + var o = repository.create(lib); + var created = repository.read(Library.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("resources/library/Library-new-library.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Library.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeletePatient() { + var p = new Patient(); + p.setId("new-patient"); + var o = repository.create(p); + var created = repository.read(Patient.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("tests/patient/Patient-new-patient.json"); + assertTrue(Files.exists(loc)); + + repository.delete(Patient.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeleteValueSet() { + var v = new ValueSet(); + v.setId("new-valueset"); + var o = repository.create(v); + var created = repository.read(ValueSet.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("vocabulary/valueset/ValueSet-new-valueset.json"); + assertTrue(Files.exists(loc)); + + repository.delete(ValueSet.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void updatePatient() { + var id = Ids.newId(Patient.class, "ABC"); + var p = repository.read(Patient.class, id); + assertFalse(p.hasActive()); + + p.setActive(true); + repository.update(p); + + var updated = repository.read(Patient.class, id); + assertTrue(updated.hasActive()); + assertTrue(updated.getActive()); + } + + @Test + void deleteNonExistentPatient() { + var id = Ids.newId(Patient.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.delete(Patient.class, id)); + } + + @Test + void searchNonExistentType() { + var results = repository.search(Bundle.class, Encounter.class, Searches.ALL); + assertNotNull(results); + assertEquals(0, results.getEntry().size()); + } + + @Test + void searchById() { + var bundle = repository.search(Bundle.class, Library.class, Searches.byId("123")); + assertNotNull(bundle); + assertEquals(1, bundle.getEntry().size()); + } + + @Test + void searchByIdNotFound() { + var bundle = repository.search(Bundle.class, Library.class, Searches.byId("DoesNotExist")); + assertNotNull(bundle); + assertEquals(0, bundle.getEntry().size()); + } + + @Test + @Order(1) // Do this test first because it puts the filesystem (temporarily) in an invalid state + void resourceMissingWhenCacheCleared() throws IOException { + var id = new IdType("Library", "ToDelete"); + var lib = new Library().setIdElement(id); + var path = tempDir.resolve("resources/library/Library-ToDelete.json"); + + repository.create(lib); + assertTrue(path.toFile().exists()); + + // Read back, should exist + lib = repository.read(Library.class, id); + assertNotNull(lib); + + // Overwrite the file on disk. + Files.writeString(path, ""); + + // Read from cache, repo doesn't know the content is gone. + lib = repository.read(Library.class, id); + assertNotNull(lib); + assertEquals("ToDelete", lib.getIdElement().getIdPart()); + + ((IgStandardRepository) repository).clearCache(); + + // Try to read again, should be gone because it's not in the cache and the content is gone. + assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); + + // Clean up so that we don't affect other tests + path.toFile().delete(); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/XmlWriteTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/XmlWriteTest.java new file mode 100644 index 00000000..7821c83d --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/XmlWriteTest.java @@ -0,0 +1,95 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import static org.junit.jupiter.api.Assertions.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.api.EncodingEnum; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.ValueSet; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.opencds.cqf.fhir.test.Resources; +import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.search.Searches; + +class XmlWriteTest { + + private static IRepository repository; + + @TempDir + static Path tempDir; + + @BeforeAll + static void setup() throws URISyntaxException, IOException, ClassNotFoundException { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/mixedEncoding", tempDir); + var conventions = IgStandardConventions.autoDetect(tempDir); + repository = new IgStandardRepository( + FhirContext.forR4Cached(), + tempDir, + conventions, + new IgStandardEncodingBehavior( + EncodingEnum.XML, IgStandardEncodingBehavior.PreserveEncoding.PRESERVE_ORIGINAL_ENCODING), + null); + } + + @Test + void createAndDeleteLibrary() { + var lib = new Library(); + lib.setId("new-library"); + var o = repository.create(lib); + var created = repository.read(Library.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("resources/library/new-library.xml"); + assertTrue(Files.exists(loc)); + + repository.delete(Library.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void createAndDeletePatient() { + var p = new Patient(); + p.setId("new-patient"); + var o = repository.create(p); + var created = repository.read(Patient.class, o.getId()); + assertNotNull(created); + + var loc = tempDir.resolve("tests/patient/new-patient.xml"); + assertTrue(Files.exists(loc)); + + repository.delete(Patient.class, created.getIdElement()); + assertFalse(Files.exists(loc)); + } + + @Test + void readExternalValueSet() { + var id = Ids.newId(ValueSet.class, "789"); + var vs = repository.read(ValueSet.class, id); + assertNotNull(vs); + assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); + + // Should be tagged with its source path + var path = (Path) vs.getUserData(IgStandardRepository.SOURCE_PATH_TAG); + assertNotNull(path); + assertTrue(path.toFile().exists()); + assertTrue(path.toString().contains("external")); + } + + @Test + void searchExternalValueSet() { + var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/789")); + assertNotNull(sets); + assertEquals(1, sets.getEntry().size()); + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.java index 78ebdc64..0b207429 100644 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.java +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.java @@ -5,7 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import org.cqframework.cql.elm.tracking.TrackBack; +import org.cqframework.cql.cql2elm.tracking.TrackBack; import org.hl7.elm.r1.Element; import org.hl7.elm.r1.ExpressionDef; import org.hl7.elm.r1.Library; diff --git a/ls/server/src/test/resources/provider/content/sample-library-1.0.0.json b/ls/server/src/test/resources/provider/content/sample-library-1.0.0.json new file mode 100644 index 00000000..1eb1024e --- /dev/null +++ b/ls/server/src/test/resources/provider/content/sample-library-1.0.0.json @@ -0,0 +1,16 @@ +{ + "resourceType": "Library", + "id": "sample-library", + "version" : "1.0.0", + "name" : "SampleLibrary", + "title" : "Sample Library", + "status" : "active", + "experimental" : true, + "type" : { + "coding" : [{ + "system" : "http://terminology.hl7.org/CodeSystem/library-type", + "code" : "logic-library", + "display" : "Logic Library" + }] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/provider/content/sample-library-2.0.0.json b/ls/server/src/test/resources/provider/content/sample-library-2.0.0.json new file mode 100644 index 00000000..7d389ff8 --- /dev/null +++ b/ls/server/src/test/resources/provider/content/sample-library-2.0.0.json @@ -0,0 +1,16 @@ +{ + "resourceType": "Library", + "id": "sample-library", + "version" : "2.0.0", + "name" : "SampleLibrary", + "title" : "Sample Library", + "status" : "active", + "experimental" : true, + "type" : { + "coding" : [{ + "system" : "http://terminology.hl7.org/CodeSystem/library-type", + "code" : "logic-library", + "display" : "Logic Library" + }] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/InvalidContent.json b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/InvalidContent.json new file mode 100644 index 00000000..0c5f31f3 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/InvalidContent.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/MissingId.json b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/MissingId.json new file mode 100644 index 00000000..5dd7c706 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/MissingId.json @@ -0,0 +1,3 @@ +{ + "resourceType": "Patient" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/NoContent.json b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/NoContent.json new file mode 100644 index 00000000..e69de29b diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/NotAFhirFile b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/NotAFhirFile new file mode 100644 index 00000000..b75e5685 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/NotAFhirFile @@ -0,0 +1 @@ +// Intentionally empty file that tests to ensure we ignore non-FHIR files \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongId.json b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongId.json new file mode 100644 index 00000000..f5620252 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongId.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Patient", + "id": "DoesntMatchFilename" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongResourceType.json b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongResourceType.json new file mode 100644 index 00000000..a5f91d61 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongResourceType.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Encounter", + "id": "WrongResourceType" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongVersion.json b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongVersion.json new file mode 100644 index 00000000..c37a88a8 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongVersion.json @@ -0,0 +1,7 @@ +{ + "resourceType": "Patient", + "id": "WrongVersion", + "meta": { + "versionId": "2" + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/resources/library/123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/resources/library/123.json new file mode 100644 index 00000000..b026b65f --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/resources/library/123.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Library", + "id": "123" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/resources/library/456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/resources/library/456.json new file mode 100644 index 00000000..7e88b654 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/resources/library/456.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Library", + "id": "456" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/123/encounter/ABC.json b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/123/encounter/ABC.json new file mode 100644 index 00000000..881455a8 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/123/encounter/ABC.json @@ -0,0 +1,7 @@ +{ + "resourceType": "Encounter", + "id": "ABC", + "subject": { + "reference": "Patient/123" + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/123/patient/123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/123/patient/123.json new file mode 100644 index 00000000..7445fac0 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/123/patient/123.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Patient", + "id": "123" +} diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/456/encounter/DEF.json b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/456/encounter/DEF.json new file mode 100644 index 00000000..5ef8a507 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/456/encounter/DEF.json @@ -0,0 +1,7 @@ +{ + "resourceType": "Encounter", + "id": "DEF", + "subject": { + "reference": "Patient/456" + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/456/patient/456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/456/patient/456.json new file mode 100644 index 00000000..4f8e71f5 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/456/patient/456.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Patient", + "id": "456" +} diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/vocabulary/valueset/456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/vocabulary/valueset/456.json new file mode 100644 index 00000000..3ad4292a --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/compartment/vocabulary/valueset/456.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "456", + "url": "example.com/ValueSet/456" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlContent/cql/Test.cql b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlContent/cql/Test.cql new file mode 100644 index 00000000..aeed429d --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlContent/cql/Test.cql @@ -0,0 +1 @@ +library Test \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/cql/measure100.cql b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/cql/measure100.cql new file mode 100644 index 00000000..e69de29b diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/cql/measure200.cql b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/cql/measure200.cql new file mode 100644 index 00000000..e69de29b diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/Encounter-1234.json b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/Encounter-1234.json new file mode 100644 index 00000000..327442f6 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/Encounter-1234.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Encounter", + "id": "1234" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/Patient-1111.json b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/Patient-1111.json new file mode 100644 index 00000000..b6f0bb57 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/Patient-1111.json @@ -0,0 +1,7 @@ +{ + "resourceType": "Patient", + "id": "1111", + "name": [ { + "family": "measure100" + } ] +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/Encounter-5678.json b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/Encounter-5678.json new file mode 100644 index 00000000..aebd3dcb --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/Encounter-5678.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Encounter", + "id": "5678" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/Patient-2222.json b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/Patient-2222.json new file mode 100644 index 00000000..188b9756 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/Patient-2222.json @@ -0,0 +1,7 @@ +{ + "resourceType": "Patient", + "id": "2222", + "name": [ { + "family": "measure100" + } ] +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/Observation-1234.json b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/Observation-1234.json new file mode 100644 index 00000000..7f69b186 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/Observation-1234.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Observation", + "id": "1234" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/Patient-1111.json b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/Patient-1111.json new file mode 100644 index 00000000..e319db3b --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/Patient-1111.json @@ -0,0 +1,7 @@ +{ + "resourceType": "Patient", + "id": "1111", + "name": [ { + "family": "measure200" + } ] +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/vocabulary/valueset/external/ValueSet-1234.json b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/vocabulary/valueset/external/ValueSet-1234.json new file mode 100644 index 00000000..b0f3f124 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/vocabulary/valueset/external/ValueSet-1234.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "1234", + "name": "ValueSet-1234" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/vocabulary/valueset/external/ValueSet-5678.json b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/vocabulary/valueset/external/ValueSet-5678.json new file mode 100644 index 00000000..cd993554 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/vocabulary/valueset/external/ValueSet-5678.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "5678", + "name": "ValueSet-5678" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/cql/Example.cql b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/cql/Example.cql new file mode 100644 index 00000000..aeed429d --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/cql/Example.cql @@ -0,0 +1 @@ +library Test \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/library/Library-123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/library/Library-123.json new file mode 100644 index 00000000..1bfad9f5 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/library/Library-123.json @@ -0,0 +1,10 @@ +{ + "resourceType": "Library", + "id": "123", + "name": "Test", + "url": "http://example.com/Library/Test", + "content": { + "contentType": "text/cql", + "url": "../../cql/Example.cql" + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/library/Library-456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/library/Library-456.json new file mode 100644 index 00000000..3adffbfd --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/library/Library-456.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Library", + "id": "456", + "name": "NotTest", + "url": "http://example.com/Library/NotTest" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/measure/Measure-XYZ.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/measure/Measure-XYZ.json new file mode 100644 index 00000000..e13b0247 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/measure/Measure-XYZ.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Measure", + "id": "XYZ" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-DEF.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-DEF.json new file mode 100644 index 00000000..65447c1d --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-DEF.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "DEF", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "12345" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-GHI.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-GHI.json new file mode 100644 index 00000000..62aa77f7 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-GHI.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "GHI", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "12345" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-JKL.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-JKL.json new file mode 100644 index 00000000..caecea4e --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-JKL.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "JKL", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "000000" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/patient/Patient-ABC.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/patient/Patient-ABC.json new file mode 100644 index 00000000..30e01de9 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/patient/Patient-ABC.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Patient", + "id": "ABC" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/vocabulary/valueset/ValueSet-456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/vocabulary/valueset/ValueSet-456.json new file mode 100644 index 00000000..3ad4292a --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/vocabulary/valueset/ValueSet-456.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "456", + "url": "example.com/ValueSet/456" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/cql/Example.cql b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/cql/Example.cql new file mode 100644 index 00000000..aeed429d --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/cql/Example.cql @@ -0,0 +1 @@ +library Test \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/library/123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/library/123.json new file mode 100644 index 00000000..1bfad9f5 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/library/123.json @@ -0,0 +1,10 @@ +{ + "resourceType": "Library", + "id": "123", + "name": "Test", + "url": "http://example.com/Library/Test", + "content": { + "contentType": "text/cql", + "url": "../../cql/Example.cql" + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/library/456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/library/456.json new file mode 100644 index 00000000..3adffbfd --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/library/456.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Library", + "id": "456", + "name": "NotTest", + "url": "http://example.com/Library/NotTest" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/measure/XYZ.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/measure/XYZ.json new file mode 100644 index 00000000..e13b0247 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/measure/XYZ.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Measure", + "id": "XYZ" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/DEF.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/DEF.json new file mode 100644 index 00000000..65447c1d --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/DEF.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "DEF", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "12345" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/GHI.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/GHI.json new file mode 100644 index 00000000..62aa77f7 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/GHI.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "GHI", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "12345" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/JKL.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/JKL.json new file mode 100644 index 00000000..caecea4e --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/JKL.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "JKL", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "000000" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/patient/ABC.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/patient/ABC.json new file mode 100644 index 00000000..30e01de9 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/patient/ABC.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Patient", + "id": "ABC" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/vocabulary/valueset/456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/vocabulary/valueset/456.json new file mode 100644 index 00000000..3ad4292a --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/vocabulary/valueset/456.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "456", + "url": "example.com/ValueSet/456" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/emptyContent/resources/patient/patient-123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/emptyContent/resources/patient/patient-123.json new file mode 100644 index 00000000..e69de29b diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/resources/library/123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/resources/library/123.json new file mode 100644 index 00000000..28e42017 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/resources/library/123.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Library", + "id": "123", + "name": "123", + "url": "http://example.com/Library/123" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/codesystem/ABC.json b/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/codesystem/ABC.json new file mode 100644 index 00000000..705973ab --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/codesystem/ABC.json @@ -0,0 +1,5 @@ +{ + "resourceType": "CodeSystem", + "id": "ABC", + "url": "example.com/CodeSystem/ABC" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/codesystem/external/DEF.json b/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/codesystem/external/DEF.json new file mode 100644 index 00000000..537f9630 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/codesystem/external/DEF.json @@ -0,0 +1,5 @@ +{ + "resourceType": "CodeSystem", + "id": "DEF", + "url": "example.com/CodeSystem/DEF" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/valueset/456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/valueset/456.json new file mode 100644 index 00000000..3ad4292a --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/valueset/456.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "456", + "url": "example.com/ValueSet/456" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/valueset/external/789.json b/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/valueset/external/789.json new file mode 100644 index 00000000..19d67a74 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/valueset/external/789.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "789", + "url": "example.com/ValueSet/789" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-DEF.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-DEF.json new file mode 100644 index 00000000..65447c1d --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-DEF.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "DEF", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "12345" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-GHI.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-GHI.json new file mode 100644 index 00000000..62aa77f7 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-GHI.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "GHI", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "12345" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-JKL.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-JKL.json new file mode 100644 index 00000000..caecea4e --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-JKL.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "JKL", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "000000" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Library-123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Library-123.json new file mode 100644 index 00000000..574a45c2 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Library-123.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Library", + "id": "123", + "name": "Test", + "url": "http://example.com/Library/Test" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Library-456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Library-456.json new file mode 100644 index 00000000..3adffbfd --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Library-456.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Library", + "id": "456", + "name": "NotTest", + "url": "http://example.com/Library/NotTest" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Measure-XYZ.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Measure-XYZ.json new file mode 100644 index 00000000..e13b0247 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Measure-XYZ.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Measure", + "id": "XYZ" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Patient-ABC.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Patient-ABC.json new file mode 100644 index 00000000..30e01de9 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/Patient-ABC.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Patient", + "id": "ABC" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flat/ValueSet-456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/ValueSet-456.json new file mode 100644 index 00000000..3ad4292a --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flat/ValueSet-456.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "456", + "url": "example.com/ValueSet/456" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/123.json new file mode 100644 index 00000000..574a45c2 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/123.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Library", + "id": "123", + "name": "Test", + "url": "http://example.com/Library/Test" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/456.json new file mode 100644 index 00000000..3adffbfd --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/456.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Library", + "id": "456", + "name": "NotTest", + "url": "http://example.com/Library/NotTest" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/789.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/789.json new file mode 100644 index 00000000..19d67a74 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/789.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "789", + "url": "example.com/ValueSet/789" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/ABC.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/ABC.json new file mode 100644 index 00000000..30e01de9 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/ABC.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Patient", + "id": "ABC" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/DEF.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/DEF.json new file mode 100644 index 00000000..65447c1d --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/DEF.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "DEF", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "12345" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/GHI.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/GHI.json new file mode 100644 index 00000000..62aa77f7 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/GHI.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "GHI", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "12345" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/JKL.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/JKL.json new file mode 100644 index 00000000..caecea4e --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/JKL.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Condition", + "id": "JKL", + "code": { + "coding": [ + { + "system": "example.com/codesystem", + "code": "000000" + } + ] + } +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/XYZ.json b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/XYZ.json new file mode 100644 index 00000000..e13b0247 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/XYZ.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Measure", + "id": "XYZ" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/misleadingFileName/resources/patient/measure-strat1-denom.json b/ls/server/src/test/resources/sampleIgs/ig/standard/misleadingFileName/resources/patient/measure-strat1-denom.json new file mode 100644 index 00000000..7d3de0bb --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/misleadingFileName/resources/patient/measure-strat1-denom.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Patient", + "id": "measure-strat1-denom" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/resources/library/123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/resources/library/123.json new file mode 100644 index 00000000..28e42017 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/resources/library/123.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Library", + "id": "123", + "name": "123", + "url": "http://example.com/Library/123" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/tests/patient/123.xml b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/tests/patient/123.xml new file mode 100644 index 00000000..8aa711ab --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/tests/patient/123.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/codesystem/ABC.xml b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/codesystem/ABC.xml new file mode 100644 index 00000000..54407d97 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/codesystem/ABC.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/codesystem/external/DEF.json b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/codesystem/external/DEF.json new file mode 100644 index 00000000..537f9630 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/codesystem/external/DEF.json @@ -0,0 +1,5 @@ +{ + "resourceType": "CodeSystem", + "id": "DEF", + "url": "example.com/CodeSystem/DEF" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/valueset/456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/valueset/456.json new file mode 100644 index 00000000..3ad4292a --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/valueset/456.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "456", + "url": "example.com/ValueSet/456" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/valueset/external/789.xml b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/valueset/external/789.xml new file mode 100644 index 00000000..10f7c74b --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/valueset/external/789.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/resources/library/123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/resources/library/123.json new file mode 100644 index 00000000..28e42017 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/resources/library/123.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Library", + "id": "123", + "name": "123", + "url": "http://example.com/Library/123" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/vocabulary/codesystem/ABC.json b/ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/vocabulary/codesystem/ABC.json new file mode 100644 index 00000000..705973ab --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/vocabulary/codesystem/ABC.json @@ -0,0 +1,5 @@ +{ + "resourceType": "CodeSystem", + "id": "ABC", + "url": "example.com/CodeSystem/ABC" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/vocabulary/valueset/456.json b/ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/vocabulary/valueset/456.json new file mode 100644 index 00000000..3ad4292a --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/vocabulary/valueset/456.json @@ -0,0 +1,5 @@ +{ + "resourceType": "ValueSet", + "id": "456", + "url": "example.com/ValueSet/456" +} \ No newline at end of file diff --git a/ls/server/src/test/resources/sampleIgs/ig/standard/nonFhirFileName/resources/patient/denom-patient-123.json b/ls/server/src/test/resources/sampleIgs/ig/standard/nonFhirFileName/resources/patient/denom-patient-123.json new file mode 100644 index 00000000..b0422f26 --- /dev/null +++ b/ls/server/src/test/resources/sampleIgs/ig/standard/nonFhirFileName/resources/patient/denom-patient-123.json @@ -0,0 +1,4 @@ +{ + "resourceType": "Patient", + "id": "denom-patient-123" +} \ No newline at end of file diff --git a/ls/service/pom.xml b/ls/service/pom.xml index fc15c444..7629bd30 100644 --- a/ls/service/pom.xml +++ b/ls/service/pom.xml @@ -2,7 +2,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - org.opencds.cqf.cql.ls cql-ls-service jar CQL Language Server Service @@ -20,17 +19,15 @@ org.opencds.cqf.cql.ls cql-ls-server - 4.0.0-SNAPSHOT + ${project.version} org.springframework.boot spring-boot - ${spring.boot.version} org.springframework.boot spring-boot-starter-logging - ${spring.boot.version} ch.qos.logback diff --git a/plugin/debug/pom.xml b/plugin/debug/pom.xml index c8336d06..2ebaf17c 100644 --- a/plugin/debug/pom.xml +++ b/plugin/debug/pom.xml @@ -2,7 +2,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - org.opencds.cqf.cql.ls cql-ls-plugin-debug jar CQL Language Server Debug Plugin @@ -20,13 +19,13 @@ org.opencds.cqf.cql.ls cql-ls-server - 4.0.0-SNAPSHOT + ${project.version} provided org.opencds.cqf.cql.debug cql-debug-server - 4.0.0-SNAPSHOT + ${project.version} org.eclipse.lsp4j diff --git a/pom.xml b/pom.xml index b7ce4cfd..7cede42f 100644 --- a/pom.xml +++ b/pom.xml @@ -12,10 +12,16 @@ https://github.com/cqframework/cql-language-server/tree/master + 17 + 17 + 17 + + 2.2.0 + UTF-8 UTF-8 - 11 - 3.23.0 + 4.0.0 + 4.2.0 1.1.1 1.7.36 2.7.18 @@ -78,6 +84,12 @@ + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + org.springframework.boot @@ -87,6 +99,7 @@ import + org.opencds.cqf.fhir cqf-fhir-bom @@ -95,6 +108,15 @@ import + + + org.cqframework + cql-bom + ${cql.version} + pom + import + + com.fasterxml.jackson jackson-bom @@ -127,15 +149,6 @@ ${jackson.version} - - - - - info.cqframework - cql-formatter - 3.27.0 - - org.eclipse.lsp4j @@ -169,8 +182,7 @@ 3.0.1 - - + org.hamcrest hamcrest-all @@ -248,7 +260,7 @@ true -XDcompilePolicy=simple - + -Xplugin:ErrorProne -XepDisableAllWarnings @@ -313,7 +325,6 @@ maven-javadoc-plugin 3.3.1 - 11 private -missing