From 9ed11915f4a23c78be0662fefbe61a7c351c4da6 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Thu, 11 Dec 2025 14:43:09 -0700 Subject: [PATCH 01/30] add support for v4 CQL --- core/pom.xml | 11 +- .../cqf/cql/ls/core/utility/Converters.java | 46 ++++ debug/server/pom.xml | 12 +- debug/service/pom.xml | 4 +- ls/server/pom.xml | 54 ++--- .../cqf/cql/ls/server/command/CqlCommand.java | 58 +++-- .../command/ViewElmCommandContribution.java | 9 +- .../manager/CacheAwareModelManager.java | 23 -- .../manager/CompilerOptionsManager.java | 8 +- .../server/manager/CqlCompilationManager.java | 9 +- .../ContentServiceModelInfoProvider.java | 24 +-- .../ContentServiceSourceProvider.java | 17 +- .../server/provider/FormattingProvider.java | 47 ++-- .../cql/ls/server/provider/HoverProvider.java | 58 ++--- .../server/utility/IgConventionsHelper.java | 201 ++++++++++++++++++ .../visitor/ExpressionTrackBackVisitor.java | 21 +- .../ExpressionTrackBackVisitorTest.java | 2 +- ls/service/pom.xml | 5 +- plugin/debug/pom.xml | 5 +- pom.xml | 41 ++-- 20 files changed, 464 insertions(+), 191 deletions(-) create mode 100644 core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.java delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CacheAwareModelManager.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java diff --git a/core/pom.xml b/core/pom.xml index fa13bd45..4abb1937 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..e78c5d8b --- /dev/null +++ b/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.java @@ -0,0 +1,46 @@ +package org.opencds.cqf.cql.ls.core.utility; + +import kotlinx.io.Buffer; +import kotlinx.io.Source; +import kotlinx.io.files.Path; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; + +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) { + resultStringBuilder.append(line).append("\n"); // Append newline if needed + } + } + return resultStringBuilder.toString(); + } + + public static java.nio.file.Path kotlinPathToJavaPath(Path kotlinPath) { + if (kotlinPath == null) { + return null; + } + // Paths.get() expects a String representing the path + return Paths.get(kotlinPath.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/debug/server/pom.xml b/debug/server/pom.xml index 7508dc9b..a5bcb616 100644 --- a/debug/server/pom.xml +++ b/debug/server/pom.xml @@ -20,7 +20,17 @@ org.opencds.cqf.cql.ls cql-ls-core - 3.9.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 4c68e439..1f5aafaf 100644 --- a/debug/service/pom.xml +++ b/debug/service/pom.xml @@ -20,17 +20,15 @@ org.opencds.cqf.cql.debug cql-debug-server - 3.9.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 f91f398a..8720b516 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 - 3.9.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,7 @@ info.picocli picocli + @@ -104,9 +109,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..d9f7e30b 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 @@ -3,15 +3,18 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.repository.IRepository; + +import java.io.IOException; import java.net.URI; +import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.stream.Stream; + 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 +23,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.utility.IgConventionsHelper; import org.opencds.cqf.fhir.cql.CqlOptions; import org.opencds.cqf.fhir.cql.Engines; import org.opencds.cqf.fhir.cql.EvaluationSettings; @@ -37,6 +41,7 @@ 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.IgConventions; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; import org.slf4j.LoggerFactory; import picocli.CommandLine; @@ -44,6 +49,10 @@ import picocli.CommandLine.Command; import picocli.CommandLine.Option; +import static kotlinx.io.files.PathsKt.Path; +import static org.opencds.cqf.cql.ls.core.utility.Converters.kotlinPathToJavaPath; +import static org.opencds.cqf.fhir.utility.repository.ig.IgConventions.STANDARD; + @Command(name = "cql", mixinStandardHelpOptions = true) public class CqlCommand implements Callable { @Option( @@ -188,8 +197,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,14 +220,23 @@ 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; - - var repository = createRepository(fhirContext, terminologyPath, modelPath); + var libraryPath = library.libraryUrl != null + ? Path(Uris.parseOrNull(library.libraryUrl).toURL().getPath()) + : null; + + //Path(Uris.parseOrNull(optionsPath).toURL().getPath()) + var modelPath = library.model != null + ? Path(Uris.parseOrNull(library.model.modelUrl).toURL().getPath()) + : null; + + var terminologyPath = library.terminologyUrl != null + ? Path(Uris.parseOrNull(library.terminologyUrl).toURL().getPath()) + : null; + + var repository = createRepository( + fhirContext, + kotlinPathToJavaPath(terminologyPath), + kotlinPathToJavaPath(modelPath)); var engine = Engines.forRepository(repository, evaluationSettings); if (library.libraryUrl != null) { @@ -261,13 +279,13 @@ private IRepository createRepository(FhirContext fhirContext, Path terminologyPa } if (modelPath != null) { - data = new IgRepository(fhirContext, modelPath); + data = new IgRepository(fhirContext, modelPath, IgConventionsHelper.autoDetect(modelPath), null); } else { data = new NoOpRepository(fhirContext); } if (terminologyPath != null) { - terminology = new IgRepository(fhirContext, terminologyPath); + terminology = new IgRepository(fhirContext, terminologyPath, IgConventionsHelper.autoDetect(terminologyPath), null); } else { terminology = new NoOpRepository(fhirContext); } @@ -277,7 +295,7 @@ 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())); } @@ -307,9 +325,9 @@ private String tempConvert(Object value) { IBaseResource resource = (IBaseResource) value; result = resource.fhirType() + (resource.getIdElement() != null - && resource.getIdElement().hasIdPart() - ? "(id=" + resource.getIdElement().getIdPart() + ")" - : ""); + && resource.getIdElement().hasIdPart() + ? "(id=" + resource.getIdElement().getIdPart() + ")" + : ""); } else if (value instanceof IBase) { result = ((IBase) value).fhirType(); } else if (value instanceof IBaseDatatype) { @@ -320,4 +338,4 @@ private String tempConvert(Object value) { return result; } -} +} \ No newline at end of file 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..ac61d7f4 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 @@ -9,7 +9,7 @@ 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 +61,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); } -} +} \ No newline at end of file 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..5e785121 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 @@ -7,7 +7,6 @@ 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; @@ -18,6 +17,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static kotlinx.io.files.PathsKt.Path; + public class CompilerOptionsManager { private static final Logger log = LoggerFactory.getLogger(CompilerOptionsManager.class); @@ -47,8 +48,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(); @@ -71,4 +71,4 @@ public void onMessageEvent(DidChangeWatchedFilesEvent event) { } } } -} +} \ No newline at end of file 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..f1a338ae 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) { @@ -92,4 +93,4 @@ private LibraryManager createLibraryManager(URI root, ModelManager modelManager) getIgContextManager().setupLibraryManager(root, libraryManager); return libraryManager; } -} +} \ No newline at end of file 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..51f8abbe 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); } @@ -61,4 +47,4 @@ public ModelInfo load(ModelIdentifier modelIdentifier) { return null; } -} +} \ No newline at end of file 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..73d2997f 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,11 +1,15 @@ package org.opencds.cqf.cql.ls.server.provider; -import java.io.InputStream; +import kotlinx.io.Source; + +import java.io.IOException; import java.net.URI; import org.cqframework.cql.cql2elm.LibrarySourceProvider; import org.hl7.elm.r1.VersionedIdentifier; import org.opencds.cqf.cql.ls.core.ContentService; +import static org.opencds.cqf.cql.ls.core.utility.Converters.inputStreamToSource; + public class ContentServiceSourceProvider implements LibrarySourceProvider { private final ContentService contentService; @@ -16,8 +20,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); + } } -} +} \ No newline at end of file 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..3e5c8f05 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 @@ -5,39 +5,42 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import org.cqframework.cql.tools.formatter.CqlFormatterVisitor; -import org.cqframework.cql.tools.formatter.CqlFormatterVisitor.FormatResult; + +//import org.cqframework.cql.tools.formatter.CqlFormatterVisitor.FormatResult; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; import org.opencds.cqf.cql.ls.core.ContentService; import org.opencds.cqf.cql.ls.core.utility.Uris; +//import static org.cqframework.cql.tools.formatter.CqlFormatterVisitor.*; + public class FormattingProvider { - private ContentService contentService; + private final ContentService contentService; public FormattingProvider(ContentService contentService) { this.contentService = contentService; } public List format(String uri) { - URI u = Objects.requireNonNull(Uris.parseOrNull(uri)); - - FormatResult fr = null; - try { - fr = CqlFormatterVisitor.getFormattedOutput(this.contentService.read(u)); - } catch (IOException e) { - throw new IllegalArgumentException("Unable to format CQL due to an error.", e); - } - - if (!fr.getErrors().isEmpty()) { - throw new IllegalArgumentException(String.join( - "\n", "Unable to format CQL due to syntax errors.", "Please fix the errors and try again.")); - } - - TextEdit te = new TextEdit( - new Range(new Position(0, 0), new Position(Integer.MAX_VALUE, Integer.MAX_VALUE)), fr.getOutput()); - - return Collections.singletonList(te); +// URI u = Objects.requireNonNull(Uris.parseOrNull(uri)); +// +// FormatResult fr; +// try { +// fr = Companion.getFormattedOutput(this.contentService.read(u)); +// } catch (IOException e) { +// throw new IllegalArgumentException("Unable to format CQL due to an error.", e); +// } +// +// if (!fr.getErrors().isEmpty()) { +// throw new IllegalArgumentException(String.join( +// "\n", "Unable to format CQL due to syntax errors.", "Please fix the errors and try again.")); +// } +// +// TextEdit te = new TextEdit( +// new Range(new Position(0, 0), new Position(Integer.MAX_VALUE, Integer.MAX_VALUE)), fr.getOutput()); +// +// return Collections.singletonList(te); + return null; } -} +} \ No newline at end of file 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..8af5930c 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,16 +3,15 @@ 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.eclipse.lsp4j.*; -import org.hl7.cql.model.DataType; import org.hl7.elm.r1.ExpressionDef; import org.hl7.elm.r1.Library.Statements; import org.opencds.cqf.cql.ls.core.utility.Uris; 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; @@ -69,21 +68,21 @@ private Pair getExpressionDefForPosition(Position position || statements.getDef().isEmpty()) { return null; } - - for (ExpressionDef def : statements.getDef()) { - if (def.getTrackbacks() == null || def.getTrackbacks().isEmpty()) { - continue; - } - - for (TrackBack tb : def.getTrackbacks()) { - if (positionInTrackBack(position, tb)) { - Range range = new Range( - new Position(tb.getStartLine() - 1, tb.getStartChar() - 1), - new Position(tb.getEndLine() - 1, tb.getEndChar())); - return Pair.of(range, def); - } - } - } +// TODO: RGT 2025-12-03 - Address missing getTrackbacks functionality +// for (ExpressionDef def : statements.getDef()) { +// if (def.getTrackbacks() == null || def.getTrackbacks().isEmpty()) { +// continue; +// } +// +// for (TrackBack tb : def.getTrackbacks()) { +// if (positionInTrackBack(position, tb)) { +// Range range = new Range( +// new Position(tb.getStartLine() - 1, tb.getStartChar() - 1), +// new Position(tb.getEndLine() - 1, tb.getEndChar())); +// return Pair.of(range, def); +// } +// } +// } return null; } @@ -105,15 +104,18 @@ public MarkupContent markup(ExpressionDef def) { return null; } - DataType resultType = def.getExpression().getResultType(); - if (resultType == null) { - return null; - } - - // Specifying the Markdown type as cql allows the client to apply - // cql syntax highlighting the resulting pop-up - String result = String.join("\n", "```cql", resultType.toString(), "```"); + return null; - return new MarkupContent("markdown", result); + // TODO - RGT -2025-12-03 - address getResultType functionality +// DataType resultType = def.getExpression().getResultType(); +// if (resultType == null) { +// return null; +// } +// +// // Specifying the Markdown type as cql allows the client to apply +// // cql syntax highlighting the resulting pop-up +// String result = String.join("\n", "```cql", resultType.toString(), "```"); +// +// return new MarkupContent("markdown", result); } -} +} \ No newline at end of file diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java new file mode 100644 index 00000000..72c0b527 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java @@ -0,0 +1,201 @@ +package org.opencds.cqf.cql.ls.server.utility; + +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.opencds.cqf.fhir.utility.repository.ig.CompartmentMode; +import org.opencds.cqf.fhir.utility.repository.ig.EncodingBehavior; +import org.opencds.cqf.fhir.utility.repository.ig.IgConventions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.opencds.cqf.fhir.utility.repository.ig.IgConventions.STANDARD; + +public class IgConventionsHelper { + + private static final Logger logger = LoggerFactory.getLogger(IgConventionsHelper.class); + + private static final List FHIR_TYPE_NAMES = Stream.of(FHIRAllTypes.values()) + .map(FHIRAllTypes::name) + .map(String::toLowerCase) + .distinct() + .collect(Collectors.toUnmodifiableList()); + + /** + * 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 IgConventions 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.collect(Collectors.toUnmodifiableList()); + + // 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(IgConventionsHelper::listFiles) + .filter(Files::isDirectory) + .anyMatch(IgConventionsHelper::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); + + // Should also check for all the file extension that are used in the IG + // e.g. .json, .xml, and add them to the enabled encodings. + + var config = new IgConventions( + hasTypeDirectory ? IgConventions.FhirTypeLayout.DIRECTORY_PER_TYPE : IgConventions.FhirTypeLayout.FLAT, + hasCategoryDirectory ? IgConventions.CategoryLayout.DIRECTORY_PER_CATEGORY : IgConventions.CategoryLayout.FLAT, + CompartmentMode.NONE, + // TODO: Cannot auto-detect this yet, default to FULL + // We can check for non-compartment resources in compartment directories to detect FHIR vs FULL + // For example, if we find a Medication resource in a Patient compartment directory, + // we know it is FULL isolation. + IgConventions.CompartmentIsolation.FULL, + hasTypeFilename ? IgConventions.FilenameMode.TYPE_AND_ID : IgConventions.FilenameMode.ID_ONLY, + EncodingBehavior.DEFAULT); + + logger.info("Auto-detected repository configuration: {}", config); + + return config; + } + + private static boolean hasTypeFilename(Path typePath) { + try (var fileStream = Files.list(typePath)) { + return fileStream + .filter(IgConventionsHelper::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()); + } + + 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 contents = Files.readString(filePath, StandardCharsets.UTF_8); + if (contents.isBlank()) { + 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(String.format("\"RESOURCETYPE\": \"%s\"", claimedFhirType.name())) + && !contents.toUpperCase() + .contains(String.format("\"ID\": \"%s\"", fileNameWithoutExtension)); + + } 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; + } + } +} 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..e59410eb 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,11 @@ 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.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,11 +36,12 @@ public Element visitRetrieve(Retrieve retrieve, TrackBack context) { } protected boolean elementCoversTrackBack(Element elm, TrackBack context) { - for (TrackBack tb : elm.getTrackbacks()) { - if (startsOnOrBefore(tb, context) && endsOnOrAfter(tb, context)) { - return true; - } - } + // TODO: RGT 2025-12-03 - Address missing getTrackbacks functionality + // for (TrackBack tb : elm.getTrackbacks()) { +// if (startsOnOrBefore(tb, context) && endsOnOrAfter(tb, context)) { +// return true; +// } +// } return false; } @@ -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; + } +} \ No newline at end of file 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/service/pom.xml b/ls/service/pom.xml index 2f5279bb..0202f6bd 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 - 3.9.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 8b0d4030..31bec502 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 - 3.9.0-SNAPSHOT + ${project.version} provided org.opencds.cqf.cql.debug cql-debug-server - 3.9.0-SNAPSHOT + ${project.version} org.eclipse.lsp4j diff --git a/pom.xml b/pom.xml index fa0ddd25..a9ceab11 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.1.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 From 39a39bb5285f238883755dc37765db72f481a5cd Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Thu, 11 Dec 2025 15:55:48 -0700 Subject: [PATCH 02/30] fixes issue with missing CqlFormattingProvider --- .../server/provider/FormattingProvider.java | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) 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 3e5c8f05..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 @@ -5,16 +5,14 @@ import java.util.Collections; import java.util.List; import java.util.Objects; - -//import org.cqframework.cql.tools.formatter.CqlFormatterVisitor.FormatResult; +import org.cqframework.cql.tools.formatter.CqlFormatterVisitor; +import org.cqframework.cql.tools.formatter.CqlFormatterVisitor.FormatResult; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.TextEdit; import org.opencds.cqf.cql.ls.core.ContentService; import org.opencds.cqf.cql.ls.core.utility.Uris; -//import static org.cqframework.cql.tools.formatter.CqlFormatterVisitor.*; - public class FormattingProvider { private final ContentService contentService; @@ -23,24 +21,23 @@ public FormattingProvider(ContentService contentService) { } public List format(String uri) { -// URI u = Objects.requireNonNull(Uris.parseOrNull(uri)); -// -// FormatResult fr; -// try { -// fr = Companion.getFormattedOutput(this.contentService.read(u)); -// } catch (IOException e) { -// throw new IllegalArgumentException("Unable to format CQL due to an error.", e); -// } -// -// if (!fr.getErrors().isEmpty()) { -// throw new IllegalArgumentException(String.join( -// "\n", "Unable to format CQL due to syntax errors.", "Please fix the errors and try again.")); -// } -// -// TextEdit te = new TextEdit( -// new Range(new Position(0, 0), new Position(Integer.MAX_VALUE, Integer.MAX_VALUE)), fr.getOutput()); -// -// return Collections.singletonList(te); - return null; + URI u = Objects.requireNonNull(Uris.parseOrNull(uri)); + + FormatResult fr; + try { + fr = CqlFormatterVisitor.Companion.getFormattedOutput(this.contentService.read(u)); + } catch (IOException e) { + throw new IllegalArgumentException("Unable to format CQL due to an error.", e); + } + + if (!fr.getErrors().isEmpty()) { + throw new IllegalArgumentException(String.join( + "\n", "Unable to format CQL due to syntax errors.", "Please fix the errors and try again.")); + } + + TextEdit te = new TextEdit( + new Range(new Position(0, 0), new Position(Integer.MAX_VALUE, Integer.MAX_VALUE)), fr.getOutput()); + + return Collections.singletonList(te); } -} \ No newline at end of file +} From 1d11683c993e1640f26f587194e34cfc9a32bb45 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Thu, 11 Dec 2025 15:56:37 -0700 Subject: [PATCH 03/30] spotless applied --- .../cqf/cql/ls/core/utility/Converters.java | 7 ++- .../cqf/cql/ls/server/command/CqlCommand.java | 33 +++++------- .../command/ViewElmCommandContribution.java | 4 +- .../manager/CompilerOptionsManager.java | 7 ++- .../server/manager/CqlCompilationManager.java | 4 +- .../ContentServiceModelInfoProvider.java | 2 +- .../ContentServiceSourceProvider.java | 7 ++- .../cql/ls/server/provider/HoverProvider.java | 52 +++++++++---------- .../server/utility/IgConventionsHelper.java | 13 ++--- .../visitor/ExpressionTrackBackVisitor.java | 10 ++-- 10 files changed, 64 insertions(+), 75 deletions(-) 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 index e78c5d8b..d098f9ef 100644 --- 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 @@ -1,15 +1,14 @@ package org.opencds.cqf.cql.ls.core.utility; -import kotlinx.io.Buffer; -import kotlinx.io.Source; -import kotlinx.io.files.Path; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; +import kotlinx.io.Buffer; +import kotlinx.io.Source; +import kotlinx.io.files.Path; public class Converters { 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 d9f7e30b..b7c61914 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,18 +1,16 @@ package org.opencds.cqf.cql.ls.server.command; +import static kotlinx.io.files.PathsKt.Path; +import static org.opencds.cqf.cql.ls.core.utility.Converters.kotlinPathToJavaPath; + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.repository.IRepository; - -import java.io.IOException; import java.net.URI; -import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; -import java.util.stream.Stream; - import org.apache.commons.lang3.tuple.Pair; import org.cqframework.cql.cql2elm.CqlTranslatorOptions; import org.cqframework.cql.cql2elm.DefaultLibrarySourceProvider; @@ -41,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.IgConventions; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; import org.slf4j.LoggerFactory; import picocli.CommandLine; @@ -49,10 +46,6 @@ import picocli.CommandLine.Command; import picocli.CommandLine.Option; -import static kotlinx.io.files.PathsKt.Path; -import static org.opencds.cqf.cql.ls.core.utility.Converters.kotlinPathToJavaPath; -import static org.opencds.cqf.fhir.utility.repository.ig.IgConventions.STANDARD; - @Command(name = "cql", mixinStandardHelpOptions = true) public class CqlCommand implements Callable { @Option( @@ -224,7 +217,7 @@ public Integer call() throws Exception { ? Path(Uris.parseOrNull(library.libraryUrl).toURL().getPath()) : null; - //Path(Uris.parseOrNull(optionsPath).toURL().getPath()) + // Path(Uris.parseOrNull(optionsPath).toURL().getPath()) var modelPath = library.model != null ? Path(Uris.parseOrNull(library.model.modelUrl).toURL().getPath()) : null; @@ -234,9 +227,7 @@ public Integer call() throws Exception { : null; var repository = createRepository( - fhirContext, - kotlinPathToJavaPath(terminologyPath), - kotlinPathToJavaPath(modelPath)); + fhirContext, kotlinPathToJavaPath(terminologyPath), kotlinPathToJavaPath(modelPath)); var engine = Engines.forRepository(repository, evaluationSettings); if (library.libraryUrl != null) { @@ -285,7 +276,8 @@ private IRepository createRepository(FhirContext fhirContext, Path terminologyPa } if (terminologyPath != null) { - terminology = new IgRepository(fhirContext, terminologyPath, IgConventionsHelper.autoDetect(terminologyPath), null); + terminology = new IgRepository( + fhirContext, terminologyPath, IgConventionsHelper.autoDetect(terminologyPath), null); } else { terminology = new NoOpRepository(fhirContext); } @@ -295,7 +287,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.getExpressionResults().entrySet()) { + for (Map.Entry libraryEntry : + result.getExpressionResults().entrySet()) { System.out.println(libraryEntry.getKey() + "=" + this.tempConvert(libraryEntry.getValue().value())); } @@ -325,9 +318,9 @@ private String tempConvert(Object value) { IBaseResource resource = (IBaseResource) value; result = resource.fhirType() + (resource.getIdElement() != null - && resource.getIdElement().hasIdPart() - ? "(id=" + resource.getIdElement().getIdPart() + ")" - : ""); + && resource.getIdElement().hasIdPart() + ? "(id=" + resource.getIdElement().getIdPart() + ")" + : ""); } else if (value instanceof IBase) { result = ((IBase) value).fhirType(); } else if (value instanceof IBaseDatatype) { @@ -338,4 +331,4 @@ private String tempConvert(Object value) { return result; } -} \ No newline at end of file +} 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 ac61d7f4..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,13 +2,11 @@ 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.ElmXmlLibraryWriter; import org.eclipse.lsp4j.ExecuteCommandParams; import org.hl7.elm.r1.Library; @@ -64,4 +62,4 @@ private static String convertToXml(Library library) throws IOException { ElmXmlLibraryWriter writer = new ElmXmlLibraryWriter(); return writer.writeAsString(library); } -} \ No newline at end of file +} 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 5e785121..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,7 +1,8 @@ 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; @@ -17,8 +18,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static kotlinx.io.files.PathsKt.Path; - public class CompilerOptionsManager { private static final Logger log = LoggerFactory.getLogger(CompilerOptionsManager.class); @@ -71,4 +70,4 @@ public void onMessageEvent(DidChangeWatchedFilesEvent event) { } } } -} \ No newline at end of file +} 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 f1a338ae..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 @@ -68,7 +68,7 @@ public CqlCompiler compile(URI uri, InputStream stream) { LibraryManager libraryManager = this.createLibraryManager(Uris.getHead(uri), modelManager); try { - CqlCompiler compiler = new CqlCompiler(null,null,libraryManager); + CqlCompiler compiler = new CqlCompiler(null, null, libraryManager); compiler.run(Converters.inputStreamToString(stream)); return compiler; } catch (IOException e) { @@ -93,4 +93,4 @@ private LibraryManager createLibraryManager(URI root, ModelManager modelManager) getIgContextManager().setupLibraryManager(root, libraryManager); return libraryManager; } -} \ No newline at end of file +} 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 51f8abbe..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 @@ -47,4 +47,4 @@ public ModelInfo load(ModelIdentifier modelIdentifier) { return null; } -} \ No newline at end of file +} 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 73d2997f..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,15 +1,14 @@ package org.opencds.cqf.cql.ls.server.provider; -import kotlinx.io.Source; +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; -import static org.opencds.cqf.cql.ls.core.utility.Converters.inputStreamToSource; - public class ContentServiceSourceProvider implements LibrarySourceProvider { private final ContentService contentService; @@ -27,4 +26,4 @@ public Source getLibrarySource(VersionedIdentifier libraryIdentifier) { throw new RuntimeException(e); } } -} \ No newline at end of file +} 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 8af5930c..834836ae 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 @@ -68,21 +68,21 @@ private Pair getExpressionDefForPosition(Position position || statements.getDef().isEmpty()) { return null; } -// TODO: RGT 2025-12-03 - Address missing getTrackbacks functionality -// for (ExpressionDef def : statements.getDef()) { -// if (def.getTrackbacks() == null || def.getTrackbacks().isEmpty()) { -// continue; -// } -// -// for (TrackBack tb : def.getTrackbacks()) { -// if (positionInTrackBack(position, tb)) { -// Range range = new Range( -// new Position(tb.getStartLine() - 1, tb.getStartChar() - 1), -// new Position(tb.getEndLine() - 1, tb.getEndChar())); -// return Pair.of(range, def); -// } -// } -// } + // TODO: RGT 2025-12-03 - Address missing getTrackbacks functionality + // for (ExpressionDef def : statements.getDef()) { + // if (def.getTrackbacks() == null || def.getTrackbacks().isEmpty()) { + // continue; + // } + // + // for (TrackBack tb : def.getTrackbacks()) { + // if (positionInTrackBack(position, tb)) { + // Range range = new Range( + // new Position(tb.getStartLine() - 1, tb.getStartChar() - 1), + // new Position(tb.getEndLine() - 1, tb.getEndChar())); + // return Pair.of(range, def); + // } + // } + // } return null; } @@ -107,15 +107,15 @@ public MarkupContent markup(ExpressionDef def) { return null; // TODO - RGT -2025-12-03 - address getResultType functionality -// DataType resultType = def.getExpression().getResultType(); -// if (resultType == null) { -// return null; -// } -// -// // Specifying the Markdown type as cql allows the client to apply -// // cql syntax highlighting the resulting pop-up -// String result = String.join("\n", "```cql", resultType.toString(), "```"); -// -// return new MarkupContent("markdown", result); + // DataType resultType = def.getExpression().getResultType(); + // if (resultType == null) { + // return null; + // } + // + // // Specifying the Markdown type as cql allows the client to apply + // // cql syntax highlighting the resulting pop-up + // String result = String.join("\n", "```cql", resultType.toString(), "```"); + // + // return new MarkupContent("markdown", result); } -} \ No newline at end of file +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java index 72c0b527..17aa1949 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java @@ -1,5 +1,7 @@ package org.opencds.cqf.cql.ls.server.utility; +import static org.opencds.cqf.fhir.utility.repository.ig.IgConventions.STANDARD; + import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -15,8 +17,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.opencds.cqf.fhir.utility.repository.ig.IgConventions.STANDARD; - public class IgConventionsHelper { private static final Logger logger = LoggerFactory.getLogger(IgConventionsHelper.class); @@ -26,7 +26,7 @@ public class IgConventionsHelper { .map(String::toLowerCase) .distinct() .collect(Collectors.toUnmodifiableList()); - + /** * 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. @@ -114,7 +114,9 @@ public static IgConventions autoDetect(Path path) { var config = new IgConventions( hasTypeDirectory ? IgConventions.FhirTypeLayout.DIRECTORY_PER_TYPE : IgConventions.FhirTypeLayout.FLAT, - hasCategoryDirectory ? IgConventions.CategoryLayout.DIRECTORY_PER_CATEGORY : IgConventions.CategoryLayout.FLAT, + hasCategoryDirectory + ? IgConventions.CategoryLayout.DIRECTORY_PER_CATEGORY + : IgConventions.CategoryLayout.FLAT, CompartmentMode.NONE, // TODO: Cannot auto-detect this yet, default to FULL // We can check for non-compartment resources in compartment directories to detect FHIR vs FULL @@ -176,8 +178,7 @@ private static boolean contentsMatchClaimedType(Path filePath, FHIRAllTypes clai // 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(String.format("\"RESOURCETYPE\": \"%s\"", claimedFhirType.name())) - && !contents.toUpperCase() - .contains(String.format("\"ID\": \"%s\"", fileNameWithoutExtension)); + && !contents.toUpperCase().contains(String.format("\"ID\": \"%s\"", fileNameWithoutExtension)); } catch (IOException e) { return false; 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 e59410eb..aa0782cd 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 @@ -38,10 +38,10 @@ public Element visitRetrieve(Retrieve retrieve, TrackBack context) { protected boolean elementCoversTrackBack(Element elm, TrackBack context) { // TODO: RGT 2025-12-03 - Address missing getTrackbacks functionality // for (TrackBack tb : elm.getTrackbacks()) { -// if (startsOnOrBefore(tb, context) && endsOnOrAfter(tb, context)) { -// return true; -// } -// } + // if (startsOnOrBefore(tb, context) && endsOnOrAfter(tb, context)) { + // return true; + // } + // } return false; } @@ -76,4 +76,4 @@ protected boolean endsOnOrAfter(TrackBack left, TrackBack right) { protected Element defaultResult(@NotNull Element element, TrackBack trackBack) { return null; } -} \ No newline at end of file +} From 0c0d2d264532316c572219feef6d7ea2c9f0e3d0 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 12 Dec 2025 07:50:37 -0700 Subject: [PATCH 04/30] fixes issues with Trackable extensions --- .../cql/ls/server/provider/HoverProvider.java | 59 +++++++++---------- .../visitor/ExpressionTrackBackVisitor.java | 12 ++-- 2 files changed, 33 insertions(+), 38 deletions(-) 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 834836ae..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 @@ -4,7 +4,9 @@ import org.apache.commons.lang3.tuple.Pair; import org.cqframework.cql.cql2elm.CqlCompiler; 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; import org.hl7.elm.r1.Library.Statements; import org.opencds.cqf.cql.ls.core.utility.Uris; @@ -42,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( @@ -63,26 +64,23 @@ 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; } - // TODO: RGT 2025-12-03 - Address missing getTrackbacks functionality - // for (ExpressionDef def : statements.getDef()) { - // if (def.getTrackbacks() == null || def.getTrackbacks().isEmpty()) { - // continue; - // } - // - // for (TrackBack tb : def.getTrackbacks()) { - // if (positionInTrackBack(position, tb)) { - // Range range = new Range( - // new Position(tb.getStartLine() - 1, tb.getStartChar() - 1), - // new Position(tb.getEndLine() - 1, tb.getEndChar())); - // return Pair.of(range, def); - // } - // } - // } + for (ExpressionDef def : statements.getDef()) { + if (Trackable.INSTANCE.getTrackbacks(def).isEmpty()) { + continue; + } + + for (TrackBack tb : Trackable.INSTANCE.getTrackbacks(def)) { + if (positionInTrackBack(position, tb)) { + Range range = new Range( + new Position(tb.getStartLine() - 1, tb.getStartChar() - 1), + new Position(tb.getEndLine() - 1, tb.getEndChar())); + return Pair.of(range, def); + } + } + } return null; } @@ -104,18 +102,15 @@ public MarkupContent markup(ExpressionDef def) { return null; } - return null; + DataType resultType = Trackable.INSTANCE.getResultType(def); + if (resultType == null) { + return null; + } - // TODO - RGT -2025-12-03 - address getResultType functionality - // DataType resultType = def.getExpression().getResultType(); - // if (resultType == null) { - // return null; - // } - // - // // Specifying the Markdown type as cql allows the client to apply - // // cql syntax highlighting the resulting pop-up - // String result = String.join("\n", "```cql", resultType.toString(), "```"); - // - // return new MarkupContent("markdown", result); + // Specifying the Markdown type as cql allows the client to apply + // cql syntax highlighting the resulting pop-up + String result = String.join("\n", "```cql", resultType.toString(), "```"); + + return new MarkupContent("markdown", result); } } 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 aa0782cd..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,6 +1,7 @@ package org.opencds.cqf.cql.ls.server.visitor; 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; @@ -36,12 +37,11 @@ public Element visitRetrieve(Retrieve retrieve, TrackBack context) { } protected boolean elementCoversTrackBack(Element elm, TrackBack context) { - // TODO: RGT 2025-12-03 - Address missing getTrackbacks functionality - // for (TrackBack tb : elm.getTrackbacks()) { - // if (startsOnOrBefore(tb, context) && endsOnOrAfter(tb, context)) { - // return true; - // } - // } + for (TrackBack tb : Trackable.INSTANCE.getTrackbacks(elm)) { + if (startsOnOrBefore(tb, context) && endsOnOrAfter(tb, context)) { + return true; + } + } return false; } From 476abd4266f0e5e698c39c241910c332cc14bdea Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Tue, 16 Dec 2025 17:37:26 -0700 Subject: [PATCH 05/30] add IgStandard Repository support --- ls/server/pom.xml | 14 + .../cqf/cql/ls/server/command/CqlCommand.java | 14 +- .../ig/standard/IgStandardConventions.java | 268 ++++++ .../ig/standard/IgStandardCqlContent.java | 73 ++ .../standard/IgStandardEncodingBehavior.java | 41 + .../ig/standard/IgStandardRepository.java | 896 ++++++++++++++++++ .../IgStandardRepositoryCompartment.java | 85 ++ .../standard/IgStandardResourceCategory.java | 25 + .../ig/standard/MultiMeasureTest.java | 41 + .../multi-measure/input/cql/measure100.cql | 0 .../multi-measure/input/cql/measure200.cql | 0 .../measure100/1111/encounter-1234.json | 4 + .../measure/measure100/1111/patient-1111.json | 7 + .../measure100/2222/encounter-5678.json | 4 + .../measure/measure100/2222/patient-2222.json | 7 + .../measure200/1111/observation-1234.json | 4 + .../measure/measure200/1111/patient-1111.json | 7 + 17 files changed, 1486 insertions(+), 4 deletions(-) create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardConventions.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardCqlContent.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardEncodingBehavior.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.java create mode 100644 ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure100.cql create mode 100644 ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure200.cql create mode 100644 ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/1111/encounter-1234.json create mode 100644 ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/1111/patient-1111.json create mode 100644 ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/2222/encounter-5678.json create mode 100644 ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/2222/patient-2222.json create mode 100644 ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure200/1111/observation-1234.json create mode 100644 ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure200/1111/patient-1111.json diff --git a/ls/server/pom.xml b/ls/server/pom.xml index 8720b516..197ff073 100644 --- a/ls/server/pom.xml +++ b/ls/server/pom.xml @@ -100,6 +100,20 @@ picocli + + + jakarta.annotation + jakarta.annotation-api + 2.1.1 + + + + + org.opencds.cqf.fhir + cqf-fhir-test + test + + 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 b7c61914..522329b9 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 @@ -226,8 +226,12 @@ public Integer call() throws Exception { ? Path(Uris.parseOrNull(library.terminologyUrl).toURL().getPath()) : null; +// var repository = createRepository( +// fhirContext, kotlinPathToJavaPath(terminologyPath), kotlinPathToJavaPath(modelPath)); + + var rootPath = Path(Uris.parseOrNull(rootDir).toURL().getPath()); var repository = createRepository( - fhirContext, kotlinPathToJavaPath(terminologyPath), kotlinPathToJavaPath(modelPath)); + fhirContext, kotlinPathToJavaPath(rootPath), kotlinPathToJavaPath(rootPath)); var engine = Engines.forRepository(repository, evaluationSettings); if (library.libraryUrl != null) { @@ -270,14 +274,16 @@ private IRepository createRepository(FhirContext fhirContext, Path terminologyPa } if (modelPath != null) { - data = new IgRepository(fhirContext, modelPath, IgConventionsHelper.autoDetect(modelPath), null); + // data = new IgRepository(fhirContext, modelPath, IgConventionsHelper.autoDetect(modelPath), null); + data = new IgRepository(fhirContext, modelPath); } else { data = new NoOpRepository(fhirContext); } if (terminologyPath != null) { - terminology = new IgRepository( - fhirContext, terminologyPath, IgConventionsHelper.autoDetect(terminologyPath), null); +// terminology = new IgRepository( +// fhirContext, terminologyPath, IgConventionsHelper.autoDetect(terminologyPath), null); + terminology = new IgRepository(fhirContext, terminologyPath); } else { terminology = new NoOpRepository(fhirContext); } 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..6b3f573b --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardConventions.java @@ -0,0 +1,268 @@ +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 or not the files are organized by resource type + * and/or category, and whether or not 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); + + /** + * Whether or not the files are organized by resource type. + */ + public enum FhirTypeLayout { + DIRECTORY_PER_TYPE, + FLAT + } + + /** + * Whether or not the resources are organized by category (tests, resources, vocabulary) + */ + public enum CategoryLayout { + DIRECTORY_PER_CATEGORY, + FLAT + } + + /** + * Whether or not the files are organized by compartment. This is primarily used for tests to + * provide isolation between test cases. + */ + public enum CompartmentLayout { + DIRECTORY_PER_COMPARTMENT, + FLAT + } + + /** + * Whether or not the files are prefixed with the resource type. + */ + 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..fc22a3e5 --- /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..39c15a7d --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.java @@ -0,0 +1,896 @@ +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 jakarta.annotation.Nullable; +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.opencds.cqf.fhir.utility.repository.ig.EncodingBehavior.PreserveEncoding; +//import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.CategoryLayout; +//import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.CompartmentLayout; +//import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.FhirTypeLayout; +//import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.FilenameMode; +//import org.opencds.cqf.fhir.utility.repository.operations.IRepositoryOperationProvider; + +/** + * 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 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 + // This 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 path = root.resolve(directory); + if (category == IgStandardResourceCategory.DATA + && this.conventions.compartmentLayout() == IgStandardConventions.CompartmentLayout.DIRECTORY_PER_COMPARTMENT) { + path = path.resolve(pathForCompartment(igStandardRepositoryCompartment)); + } + + return path; + } + + /** + * 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) { + var file = path.toFile(); + if (!file.exists()) { + return null; + } + + var extension = fileExtension(path); + if (extension == null) { + 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()); + + 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) { + return o; + } else { + var resource = readResource(path); + this.resourceCache.put(path, 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"); + + var compartment = compartmentFrom(headers); + + var paths = this.potentialPathsForResource(resourceType, id, compartment); + for (var path : paths) { + if (!path.toFile().exists()) { + continue; + } + + var resource = cachedReadResource(path); + if (resource != null) { + return validateResource(resourceType, resource, 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); + } + + // Patient context is a special-case. We don't tack the compartment context on + // the end of the path. We just use the id as the directory. + protected String pathForCompartment(IgStandardRepositoryCompartment igStandardRepositoryCompartment) { + if (igStandardRepositoryCompartment.isEmpty()) { + return ""; + } + + 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..5ab584c3 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.java @@ -0,0 +1,85 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; +import java.util.StringJoiner; + +/** + * 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..d1ea0e08 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.java @@ -0,0 +1,25 @@ +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/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..6a22ade2 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.java @@ -0,0 +1,41 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +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; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +public class MultiMeasureTest { + + private static String resourcePath = "/sample-igs/ig/standard/cql-measures/multi-measure"; + 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(resourcePath, tempDir); + repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); + } + + @Test + void readMeasure100Resources() { + var id = Ids.newId(Library.class, "DoesNotExist"); + assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); + } + +} diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure100.cql b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure100.cql new file mode 100644 index 00000000..e69de29b diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure200.cql b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure200.cql new file mode 100644 index 00000000..e69de29b diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/1111/encounter-1234.json b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/1111/encounter-1234.json new file mode 100644 index 00000000..327442f6 --- /dev/null +++ b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/1111/patient-1111.json b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/1111/patient-1111.json new file mode 100644 index 00000000..b6f0bb57 --- /dev/null +++ b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/2222/encounter-5678.json b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/2222/encounter-5678.json new file mode 100644 index 00000000..aebd3dcb --- /dev/null +++ b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/2222/patient-2222.json b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/2222/patient-2222.json new file mode 100644 index 00000000..188b9756 --- /dev/null +++ b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure200/1111/observation-1234.json b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure200/1111/observation-1234.json new file mode 100644 index 00000000..7f69b186 --- /dev/null +++ b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure200/1111/patient-1111.json b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure200/1111/patient-1111.json new file mode 100644 index 00000000..e319db3b --- /dev/null +++ b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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 From 0037cf46b4f91762421bff11543bd603d6a2ca93 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Thu, 18 Dec 2025 16:43:02 -0700 Subject: [PATCH 06/30] updates IgStandard Repository --- .../cqf/cql/ls/server/command/CqlCommand.java | 17 ++---- .../ig/standard/IgStandardConventions.java | 8 +-- .../standard/IgStandardEncodingBehavior.java | 8 +-- .../ig/standard/IgStandardRepository.java | 27 +++++---- .../IgStandardRepositoryCompartment.java | 3 +- .../standard/IgStandardResourceCategory.java | 1 - .../ig/standard/MultiMeasureTest.java | 55 +++++++++++++++---- .../valueset/external/ValueSet-1234.json | 5 ++ .../valueset/external/ValueSet-5678.json | 5 ++ pom.xml | 2 +- 10 files changed, 81 insertions(+), 50 deletions(-) create mode 100644 ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/vocabulary/valueset/external/ValueSet-1234.json create mode 100644 ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/vocabulary/valueset/external/ValueSet-5678.json 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 522329b9..77589519 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 @@ -25,7 +25,7 @@ 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.utility.IgConventionsHelper; +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; @@ -39,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; @@ -226,12 +225,9 @@ public Integer call() throws Exception { ? Path(Uris.parseOrNull(library.terminologyUrl).toURL().getPath()) : null; -// var repository = createRepository( -// fhirContext, kotlinPathToJavaPath(terminologyPath), kotlinPathToJavaPath(modelPath)); - - var rootPath = Path(Uris.parseOrNull(rootDir).toURL().getPath()); var repository = createRepository( - fhirContext, kotlinPathToJavaPath(rootPath), kotlinPathToJavaPath(rootPath)); + fhirContext, kotlinPathToJavaPath(terminologyPath), kotlinPathToJavaPath(modelPath)); + var engine = Engines.forRepository(repository, evaluationSettings); if (library.libraryUrl != null) { @@ -274,16 +270,13 @@ private IRepository createRepository(FhirContext fhirContext, Path terminologyPa } if (modelPath != null) { - // data = new IgRepository(fhirContext, modelPath, IgConventionsHelper.autoDetect(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, IgConventionsHelper.autoDetect(terminologyPath), null); - terminology = new IgRepository(fhirContext, terminologyPath); + terminology = new IgStandardRepository(fhirContext, terminologyPath); } else { terminology = new NoOpRepository(fhirContext); } 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 index 6b3f573b..a0c4a4b2 100644 --- 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 @@ -60,10 +60,7 @@ public enum FilenameMode { } public static final IgStandardConventions FLAT = new IgStandardConventions( - FhirTypeLayout.FLAT, - CategoryLayout.FLAT, - CompartmentLayout.FLAT, - FilenameMode.TYPE_AND_ID); + FhirTypeLayout.FLAT, CategoryLayout.FLAT, CompartmentLayout.FLAT, FilenameMode.TYPE_AND_ID); public static final IgStandardConventions STANDARD = new IgStandardConventions( FhirTypeLayout.DIRECTORY_PER_TYPE, @@ -219,7 +216,7 @@ private static boolean contentsMatchClaimedType(Path filePath, FHIRAllTypes clai // 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())); + .contains("\"ID\": \"%s\"".formatted(fileNameWithoutExtension.toUpperCase())); } catch (IOException e) { return false; @@ -265,4 +262,3 @@ public String toString() { .formatted(typeLayout, categoryLayout, compartmentLayout, filenameMode); } } - 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 index fc22a3e5..9c402f73 100644 --- 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 @@ -19,13 +19,14 @@ public enum PreserveEncoding { OVERWRITE_WITH_PREFERRED_ENCODING } - public static final IgStandardEncodingBehavior DEFAULT = - new IgStandardEncodingBehavior(EncodingEnum.JSON, IgStandardEncodingBehavior.PreserveEncoding.PRESERVE_ORIGINAL_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) { + public IgStandardEncodingBehavior( + EncodingEnum preferredEncoding, IgStandardEncodingBehavior.PreserveEncoding preserveEncoding) { this.preferredEncoding = preferredEncoding; this.preserveEncoding = preserveEncoding; } @@ -38,4 +39,3 @@ 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 index 39c15a7d..c7fb99bb 100644 --- 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 @@ -20,7 +20,7 @@ import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Multimap; -//import jakarta.annotation.Nullable; +// import jakarta.annotation.Nullable; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; @@ -43,12 +43,12 @@ 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.opencds.cqf.fhir.utility.repository.ig.EncodingBehavior.PreserveEncoding; -//import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.CategoryLayout; -//import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.CompartmentLayout; -//import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.FhirTypeLayout; -//import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.FilenameMode; -//import org.opencds.cqf.fhir.utility.repository.operations.IRepositoryOperationProvider; +// import org.opencds.cqf.fhir.utility.repository.ig.EncodingBehavior.PreserveEncoding; +// import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.CategoryLayout; +// import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.CompartmentLayout; +// import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.FhirTypeLayout; +// import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.FilenameMode; +// import org.opencds.cqf.fhir.utility.repository.operations.IRepositoryOperationProvider; /** * Provides access to FHIR resources stored in a directory structure following @@ -113,7 +113,7 @@ public class IgStandardRepository implements IRepository { // Directory names static final String EXTERNAL_DIRECTORY = "external"; static final Map CATEGORY_DIRECTORIES = new ImmutableMap.Builder< - IgStandardResourceCategory, String>() + IgStandardResourceCategory, String>() .put(IgStandardResourceCategory.CONTENT, "resources") .put(IgStandardResourceCategory.DATA, "tests") .put(IgStandardResourceCategory.TERMINOLOGY, "vocabulary") @@ -244,7 +244,8 @@ protected List potentialPaths potentialDirectories.add(directory); // Currently, only terminology resources are allowed to be external - if (IgStandardResourceCategory.forType(resourceType.getSimpleName()) == IgStandardResourceCategory.TERMINOLOGY) { + if (IgStandardResourceCategory.forType(resourceType.getSimpleName()) + == IgStandardResourceCategory.TERMINOLOGY) { var externalDirectory = directory.resolve(EXTERNAL_DIRECTORY); potentialDirectories.add(externalDirectory); } @@ -302,7 +303,8 @@ protected Path directoryForCategory( var directory = CATEGORY_DIRECTORIES.get(category); var path = root.resolve(directory); if (category == IgStandardResourceCategory.DATA - && this.conventions.compartmentLayout() == IgStandardConventions.CompartmentLayout.DIRECTORY_PER_COMPARTMENT) { + && this.conventions.compartmentLayout() + == IgStandardConventions.CompartmentLayout.DIRECTORY_PER_COMPARTMENT) { path = path.resolve(pathForCompartment(igStandardRepositoryCompartment)); } @@ -349,7 +351,7 @@ protected Path directoryForResource( * @return An {@code Optional} containing the resource if found; otherwise, * empty. */ -// @Nullable + // @Nullable protected IBaseResource readResource(Path path) { var file = path.toFile(); if (!file.exists()) { @@ -673,7 +675,8 @@ public MethodOutcome update(T resource, Map repository.read(Library.class, id)); + 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 + 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/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/vocabulary/valueset/external/ValueSet-1234.json b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/vocabulary/valueset/external/ValueSet-1234.json new file mode 100644 index 00000000..b0f3f124 --- /dev/null +++ b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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/sample-igs/ig/standard/cql-measures/multi-measure/input/vocabulary/valueset/external/ValueSet-5678.json b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/vocabulary/valueset/external/ValueSet-5678.json new file mode 100644 index 00000000..cd993554 --- /dev/null +++ b/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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/pom.xml b/pom.xml index a9ceab11..a33f1c25 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ UTF-8 UTF-8 4.0.0 - 4.1.0 + 4.2.0 1.1.1 1.7.36 2.7.18 From c5f5bbd3d48fb571f4e0a406c8d807a477471ddf Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Thu, 18 Dec 2025 16:59:01 -0700 Subject: [PATCH 07/30] removes unused IgConventionsHelper --- .../server/utility/IgConventionsHelper.java | 202 ------------------ 1 file changed, 202 deletions(-) delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java deleted file mode 100644 index 17aa1949..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/IgConventionsHelper.java +++ /dev/null @@ -1,202 +0,0 @@ -package org.opencds.cqf.cql.ls.server.utility; - -import static org.opencds.cqf.fhir.utility.repository.ig.IgConventions.STANDARD; - -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.opencds.cqf.fhir.utility.repository.ig.CompartmentMode; -import org.opencds.cqf.fhir.utility.repository.ig.EncodingBehavior; -import org.opencds.cqf.fhir.utility.repository.ig.IgConventions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class IgConventionsHelper { - - private static final Logger logger = LoggerFactory.getLogger(IgConventionsHelper.class); - - private static final List FHIR_TYPE_NAMES = Stream.of(FHIRAllTypes.values()) - .map(FHIRAllTypes::name) - .map(String::toLowerCase) - .distinct() - .collect(Collectors.toUnmodifiableList()); - - /** - * 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 IgConventions 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.collect(Collectors.toUnmodifiableList()); - - // 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(IgConventionsHelper::listFiles) - .filter(Files::isDirectory) - .anyMatch(IgConventionsHelper::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); - - // Should also check for all the file extension that are used in the IG - // e.g. .json, .xml, and add them to the enabled encodings. - - var config = new IgConventions( - hasTypeDirectory ? IgConventions.FhirTypeLayout.DIRECTORY_PER_TYPE : IgConventions.FhirTypeLayout.FLAT, - hasCategoryDirectory - ? IgConventions.CategoryLayout.DIRECTORY_PER_CATEGORY - : IgConventions.CategoryLayout.FLAT, - CompartmentMode.NONE, - // TODO: Cannot auto-detect this yet, default to FULL - // We can check for non-compartment resources in compartment directories to detect FHIR vs FULL - // For example, if we find a Medication resource in a Patient compartment directory, - // we know it is FULL isolation. - IgConventions.CompartmentIsolation.FULL, - hasTypeFilename ? IgConventions.FilenameMode.TYPE_AND_ID : IgConventions.FilenameMode.ID_ONLY, - EncodingBehavior.DEFAULT); - - logger.info("Auto-detected repository configuration: {}", config); - - return config; - } - - private static boolean hasTypeFilename(Path typePath) { - try (var fileStream = Files.list(typePath)) { - return fileStream - .filter(IgConventionsHelper::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()); - } - - 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 contents = Files.readString(filePath, StandardCharsets.UTF_8); - if (contents.isBlank()) { - 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(String.format("\"RESOURCETYPE\": \"%s\"", claimedFhirType.name())) - && !contents.toUpperCase().contains(String.format("\"ID\": \"%s\"", fileNameWithoutExtension)); - - } 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; - } - } -} From 1ab51c4856d61712354d33a53caefbda685795e4 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 19 Dec 2025 10:12:53 -0700 Subject: [PATCH 08/30] code clean-up --- .../repository/ig/standard/IgStandardRepository.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 index c7fb99bb..97c52484 100644 --- 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 @@ -20,7 +20,7 @@ import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Multimap; -// import jakarta.annotation.Nullable; + import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; @@ -43,12 +43,6 @@ 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.opencds.cqf.fhir.utility.repository.ig.EncodingBehavior.PreserveEncoding; -// import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.CategoryLayout; -// import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.CompartmentLayout; -// import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.FhirTypeLayout; -// import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.FilenameMode; -// import org.opencds.cqf.fhir.utility.repository.operations.IRepositoryOperationProvider; /** * Provides access to FHIR resources stored in a directory structure following From 3e0918b0763065be7246e89a83362ccd8d896162 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 19 Dec 2025 10:13:29 -0700 Subject: [PATCH 09/30] logging for test debugging when run in github --- .../ig/standard/MultiMeasureTest.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) 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 index 4013ca00..902278ca 100644 --- 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 @@ -15,8 +15,11 @@ 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 = "/sample-igs/ig/standard/cql-measures/multi-measure"; private static final String modelPathMeasure100TestCase1111 = "input/tests/measure/measure100/1111"; @@ -37,13 +40,25 @@ static void setup() throws URISyntaxException, IOException, ClassNotFoundExcepti // This copies the sample IG to a temporary directory so that // we can test against an actual filesystem Resources.copyFromJar(rootDir, tempDir); + + var pathModelPathMeasure100TestCase1111 = tempDir.resolve(modelPathMeasure100TestCase1111); + var pathModelPathMeasure100TestCase2222 = tempDir.resolve(modelPathMeasure100TestCase2222); + var pathModelPathMeasure200TestCase1111 = tempDir.resolve(modelPathMeasure200TestCase1111); + var pathTerminology = tempDir.resolve(terminologyPath); + model1111Measure100Repo = - new IgStandardRepository(FhirContext.forR4Cached(), tempDir.resolve(modelPathMeasure100TestCase1111)); + new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure100TestCase1111); model2222Measure100Repo = - new IgStandardRepository(FhirContext.forR4Cached(), tempDir.resolve(modelPathMeasure100TestCase2222)); + new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure100TestCase2222); model1111Measure200Repo = - new IgStandardRepository(FhirContext.forR4Cached(), tempDir.resolve(modelPathMeasure200TestCase1111)); - terminologyRepo = new IgStandardRepository(FhirContext.forR4Cached(), tempDir.resolve(terminologyPath)); + new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure200TestCase1111); + terminologyRepo = new IgStandardRepository(FhirContext.forR4Cached(), pathTerminology); + + log.debug("tempDir[{}] exists: {}", tempDir, tempDir.toFile().exists()); + log.debug("measure 100 patient 1111 dir[{}] exists: {}", pathModelPathMeasure100TestCase1111, pathModelPathMeasure100TestCase1111.toFile().exists()); + log.debug("measure 100 patient 2222 dir[{}] exists: {}", pathModelPathMeasure100TestCase2222, pathModelPathMeasure100TestCase2222.toFile().exists()); + log.debug("measure 200 patient 1111 dir[{}] exists: {}", pathModelPathMeasure200TestCase1111, pathModelPathMeasure200TestCase1111.toFile().exists()); + log.debug("terminology dir[{}] exists: {}", pathTerminology, pathTerminology.toFile().exists()); } @Test From 399be175d259d72d0e5cd2cd542bbe9d3b91fa9f Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 19 Dec 2025 10:23:34 -0700 Subject: [PATCH 10/30] disabled test --- .../cql/ls/server/repository/ig/standard/MultiMeasureTest.java | 3 +++ 1 file changed, 3 insertions(+) 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 index 902278ca..813775e1 100644 --- 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 @@ -11,6 +11,7 @@ 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.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.opencds.cqf.fhir.test.Resources; @@ -70,6 +71,8 @@ void should_throwException_when_libraryDoesNotExist() { 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"); From 1fab7a6b923b723aed1206e29977e81de3219755 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 19 Dec 2025 10:51:04 -0700 Subject: [PATCH 11/30] spotless applied --- .../ig/standard/IgStandardRepository.java | 1 - .../ig/standard/MultiMeasureTest.java | 20 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) 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 index 97c52484..6fa41db6 100644 --- 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 @@ -20,7 +20,6 @@ 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; 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 index 813775e1..d63002f9 100644 --- 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 @@ -56,10 +56,22 @@ static void setup() throws URISyntaxException, IOException, ClassNotFoundExcepti terminologyRepo = new IgStandardRepository(FhirContext.forR4Cached(), pathTerminology); log.debug("tempDir[{}] exists: {}", tempDir, tempDir.toFile().exists()); - log.debug("measure 100 patient 1111 dir[{}] exists: {}", pathModelPathMeasure100TestCase1111, pathModelPathMeasure100TestCase1111.toFile().exists()); - log.debug("measure 100 patient 2222 dir[{}] exists: {}", pathModelPathMeasure100TestCase2222, pathModelPathMeasure100TestCase2222.toFile().exists()); - log.debug("measure 200 patient 1111 dir[{}] exists: {}", pathModelPathMeasure200TestCase1111, pathModelPathMeasure200TestCase1111.toFile().exists()); - log.debug("terminology dir[{}] exists: {}", pathTerminology, pathTerminology.toFile().exists()); + log.debug( + "measure 100 patient 1111 dir[{}] exists: {}", + pathModelPathMeasure100TestCase1111, + pathModelPathMeasure100TestCase1111.toFile().exists()); + log.debug( + "measure 100 patient 2222 dir[{}] exists: {}", + pathModelPathMeasure100TestCase2222, + pathModelPathMeasure100TestCase2222.toFile().exists()); + log.debug( + "measure 200 patient 1111 dir[{}] exists: {}", + pathModelPathMeasure200TestCase1111, + pathModelPathMeasure200TestCase1111.toFile().exists()); + log.debug( + "terminology dir[{}] exists: {}", + pathTerminology, + pathTerminology.toFile().exists()); } @Test From 0c60a6acaf2985ca84d3f70fa7dc6af3154cbe97 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 19 Dec 2025 15:43:49 -0700 Subject: [PATCH 12/30] adds converters tests --- .../cqf/cql/ls/core/utility/Converters.java | 13 +---- .../cql/ls/core/utility/ConvertersTest.java | 57 +++++++++++++++++++ .../cqf/cql/ls/server/command/CqlCommand.java | 20 ++++--- .../ig/standard/MultiMeasureTest.java | 10 ++-- 4 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.java 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 index d098f9ef..19f47637 100644 --- 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 @@ -5,10 +5,8 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; import kotlinx.io.Buffer; import kotlinx.io.Source; -import kotlinx.io.files.Path; public class Converters { @@ -17,20 +15,13 @@ public static String inputStreamToString(InputStream inputStream) throws IOExcep try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { String line; while ((line = br.readLine()) != null) { - resultStringBuilder.append(line).append("\n"); // Append newline if needed + if (!resultStringBuilder.isEmpty()) resultStringBuilder.append("\n"); // Append newline if needed + resultStringBuilder.append(line); } } return resultStringBuilder.toString(); } - public static java.nio.file.Path kotlinPathToJavaPath(Path kotlinPath) { - if (kotlinPath == null) { - return null; - } - // Paths.get() expects a String representing the path - return Paths.get(kotlinPath.toString()); - } - public static Source stringToSource(String text) { Buffer buffer = new Buffer(); // Write the string to the buffer using a specific character encoding 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..2511eedc --- /dev/null +++ b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.java @@ -0,0 +1,57 @@ +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_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(actual, expected); + } 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/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 77589519..91d55832 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,13 +1,13 @@ package org.opencds.cqf.cql.ls.server.command; import static kotlinx.io.files.PathsKt.Path; -import static org.opencds.cqf.cql.ls.core.utility.Converters.kotlinPathToJavaPath; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.repository.IRepository; import java.net.URI; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -212,32 +212,34 @@ public Integer call() throws Exception { evaluationSettings.setNpmProcessor(new NpmProcessor(igContext)); for (LibraryParameter library : libraries) { - var libraryPath = library.libraryUrl != 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; - // Path(Uris.parseOrNull(optionsPath).toURL().getPath()) var modelPath = library.model != null - ? Path(Uris.parseOrNull(library.model.modelUrl).toURL().getPath()) + ? Paths.get(Uris.parseOrNull(library.model.modelUrl).toURL().getPath()) : null; var terminologyPath = library.terminologyUrl != null - ? Path(Uris.parseOrNull(library.terminologyUrl).toURL().getPath()) + ? Paths.get(Uris.parseOrNull(library.terminologyUrl).toURL().getPath()) : null; - var repository = createRepository( - fhirContext, kotlinPathToJavaPath(terminologyPath), kotlinPathToJavaPath(modelPath)); + 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() 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 index d63002f9..9ec651bd 100644 --- 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 @@ -55,20 +55,20 @@ static void setup() throws URISyntaxException, IOException, ClassNotFoundExcepti new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure200TestCase1111); terminologyRepo = new IgStandardRepository(FhirContext.forR4Cached(), pathTerminology); - log.debug("tempDir[{}] exists: {}", tempDir, tempDir.toFile().exists()); - log.debug( + log.info("tempDir[{}] exists: {}", tempDir, tempDir.toFile().exists()); + log.info( "measure 100 patient 1111 dir[{}] exists: {}", pathModelPathMeasure100TestCase1111, pathModelPathMeasure100TestCase1111.toFile().exists()); - log.debug( + log.info( "measure 100 patient 2222 dir[{}] exists: {}", pathModelPathMeasure100TestCase2222, pathModelPathMeasure100TestCase2222.toFile().exists()); - log.debug( + log.info( "measure 200 patient 1111 dir[{}] exists: {}", pathModelPathMeasure200TestCase1111, pathModelPathMeasure200TestCase1111.toFile().exists()); - log.debug( + log.info( "terminology dir[{}] exists: {}", pathTerminology, pathTerminology.toFile().exists()); From df70e890134a38075c9675a005e3af7f34678dc3 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 19 Dec 2025 16:05:36 -0700 Subject: [PATCH 13/30] adds converters multi-line test --- .../cqf/cql/ls/core/utility/ConvertersTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 index 2511eedc..d7a6aa27 100644 --- 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 @@ -24,6 +24,20 @@ void should_returnString_when_inputStreamExists() { } } + @Test + void should_returnStringWithLineBreaks_when_inputStreamHasLineBreaksExists() { + var expected = "the first day in spring –\n" + + "a wind from the ocean\n" + + "but no ocean in sight"; + try { + var actual = + Converters.inputStreamToString(new ByteArrayInputStream(expected.getBytes(StandardCharsets.UTF_8))); + assertEquals(actual, expected); + } catch (IOException e) { + fail("Unexpected exception thrown. {}", e); + } + } + @Test void should_throwIOException_when_inputStreamToStringHasAnIOError() throws IOException { InputStream inputStream = mock(InputStream.class); From 5b3943da4ae975891ba537e555fca8670057e1c9 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 19 Dec 2025 16:08:46 -0700 Subject: [PATCH 14/30] spotless applied --- .../org/opencds/cqf/cql/ls/core/utility/ConvertersTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 index d7a6aa27..1b7e8a86 100644 --- 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 @@ -26,9 +26,7 @@ void should_returnString_when_inputStreamExists() { @Test void should_returnStringWithLineBreaks_when_inputStreamHasLineBreaksExists() { - var expected = "the first day in spring –\n" + - "a wind from the ocean\n" + - "but no ocean in sight"; + var expected = "the first day in spring –\n" + "a wind from the ocean\n" + "but no ocean in sight"; try { var actual = Converters.inputStreamToString(new ByteArrayInputStream(expected.getBytes(StandardCharsets.UTF_8))); From e717380165ede486ffee5624e7ef574f430c5ab8 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 19 Dec 2025 16:19:02 -0700 Subject: [PATCH 15/30] enabled resource/repo test --- .../cql/ls/server/repository/ig/standard/MultiMeasureTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 9ec651bd..07bf8d77 100644 --- 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 @@ -11,7 +11,6 @@ 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.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.opencds.cqf.fhir.test.Resources; @@ -84,7 +83,7 @@ void should_throwException_when_libraryDoesNotExist() { } // Test works locally but doesn't work on github - @Disabled("Disabled until issue with running test on github is resolved.") + // @Disabled("Disabled until issue with running test on github is resolved.") @Test void should_findResourceInCorrectRepo_when_resourcesIsolatedByRepo() { var id = Ids.newId(Patient.class, "1111"); From acc71c6e46e79e6e97001ffa09f3e8def25c7c1c Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 19 Dec 2025 17:14:07 -0700 Subject: [PATCH 16/30] more testing logs - debugging github issues --- .../ig/standard/MultiMeasureTest.java | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) 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 index 07bf8d77..797de66c 100644 --- 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 @@ -7,7 +7,10 @@ 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; @@ -35,6 +38,19 @@ public class MultiMeasureTest { 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 @@ -54,23 +70,11 @@ static void setup() throws URISyntaxException, IOException, ClassNotFoundExcepti new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure200TestCase1111); terminologyRepo = new IgStandardRepository(FhirContext.forR4Cached(), pathTerminology); - log.info("tempDir[{}] exists: {}", tempDir, tempDir.toFile().exists()); - log.info( - "measure 100 patient 1111 dir[{}] exists: {}", - pathModelPathMeasure100TestCase1111, - pathModelPathMeasure100TestCase1111.toFile().exists()); - log.info( - "measure 100 patient 2222 dir[{}] exists: {}", - pathModelPathMeasure100TestCase2222, - pathModelPathMeasure100TestCase2222.toFile().exists()); - log.info( - "measure 200 patient 1111 dir[{}] exists: {}", - pathModelPathMeasure200TestCase1111, - pathModelPathMeasure200TestCase1111.toFile().exists()); - log.info( - "terminology dir[{}] exists: {}", - pathTerminology, - pathTerminology.toFile().exists()); + listFiles(tempDir); + listFiles(pathModelPathMeasure100TestCase1111); + listFiles(pathModelPathMeasure100TestCase2222); + listFiles(pathModelPathMeasure200TestCase1111); + listFiles(pathTerminology); } @Test From 44f3aafce9bbd1e3969375966bfe16c08a4bfad0 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 19 Dec 2025 17:26:50 -0700 Subject: [PATCH 17/30] added @TempDir - debugging github issues --- .../ig/standard/MultiMeasureTest.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) 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 index 797de66c..8e5e8a77 100644 --- 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 @@ -33,6 +33,18 @@ public class MultiMeasureTest { @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; @@ -57,10 +69,10 @@ static void setup() throws URISyntaxException, IOException, ClassNotFoundExcepti // we can test against an actual filesystem Resources.copyFromJar(rootDir, tempDir); - var pathModelPathMeasure100TestCase1111 = tempDir.resolve(modelPathMeasure100TestCase1111); - var pathModelPathMeasure100TestCase2222 = tempDir.resolve(modelPathMeasure100TestCase2222); - var pathModelPathMeasure200TestCase1111 = tempDir.resolve(modelPathMeasure200TestCase1111); - var pathTerminology = tempDir.resolve(terminologyPath); + pathModelPathMeasure100TestCase1111 = tempDir.resolve(modelPathMeasure100TestCase1111); + pathModelPathMeasure100TestCase2222 = tempDir.resolve(modelPathMeasure100TestCase2222); + pathModelPathMeasure200TestCase1111 = tempDir.resolve(modelPathMeasure200TestCase1111); + pathTerminology = tempDir.resolve(terminologyPath); model1111Measure100Repo = new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure100TestCase1111); From e0ea9f0a6aa281e3b78458a901fd05665a7e719f Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Sat, 20 Dec 2025 08:57:53 -0700 Subject: [PATCH 18/30] adds CQL IgStandardContent tests --- .../ig/standard/CqlIgStandardContentTest.java | 95 +++++++++++++++++++ .../resources/cqlContentTest/cql/Test.cql | 1 + 2 files changed, 96 insertions(+) create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlIgStandardContentTest.java create mode 100644 ls/server/src/test/resources/cqlContentTest/cql/Test.cql diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlIgStandardContentTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlIgStandardContentTest.java new file mode 100644 index 00000000..92880d25 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlIgStandardContentTest.java @@ -0,0 +1,95 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard; + +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +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; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class CqlIgStandardContentTest { + + @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("/cqlContentTest", 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/resources/cqlContentTest/cql/Test.cql b/ls/server/src/test/resources/cqlContentTest/cql/Test.cql new file mode 100644 index 00000000..aeed429d --- /dev/null +++ b/ls/server/src/test/resources/cqlContentTest/cql/Test.cql @@ -0,0 +1 @@ +library Test \ No newline at end of file From 5a9b25b3ddc3d6c67104c9f906a0e1809e2a8a54 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Sat, 20 Dec 2025 09:37:13 -0700 Subject: [PATCH 19/30] adds IgStandardRepo test coverage --- .../repository/ig/standard/BadDataTest.java | 70 +++++ .../ig/standard/CompartmentTest.java | 243 ++++++++++++++++++ ...rdContentTest.java => CqlContentTest.java} | 35 +-- .../repository/ig/standard/DirectoryTest.java | 176 +++++++++++++ .../repository/ig/standard/ExternalTest.java | 87 +++++++ .../ig/standard/FlatNoTypeNamesTest.java | 176 +++++++++++++ .../repository/ig/standard/FlatTest.java | 176 +++++++++++++ .../ig/standard/MixedEncodingTest.java | 181 +++++++++++++ .../ig/standard/MultiMeasureTest.java | 2 +- .../ig/standard/NoTestDataTest.java | 141 ++++++++++ .../ig/standard/OverwriteEncodingTest.java | 62 +++++ .../repository/ig/standard/PrefixTest.java | 221 ++++++++++++++++ .../repository/ig/standard/XmlWriteTest.java | 95 +++++++ .../badData/tests/patient/InvalidContent.json | 1 + .../badData/tests/patient/MissingId.json | 3 + .../badData/tests/patient/NoContent.json} | 0 .../badData/tests/patient/NotAFhirFile | 1 + .../badData/tests/patient/WrongId.json | 4 + .../tests/patient/WrongResourceType.json | 4 + .../badData/tests/patient/WrongVersion.json | 7 + .../compartment/resources/library/123.json | 4 + .../compartment/resources/library/456.json | 4 + .../tests/patient/123/encounter/ABC.json | 7 + .../tests/patient/123/patient/123.json | 4 + .../tests/patient/456/encounter/DEF.json | 7 + .../tests/patient/456/patient/456.json | 4 + .../compartment/vocabulary/valueset/456.json | 5 + .../ig/standard/cqlContent}/cql/Test.cql | 0 .../multiMeasure/input/cql/measure100.cql} | 0 .../multiMeasure/input/cql/measure200.cql | 0 .../measure100/1111/encounter-1234.json | 0 .../measure/measure100/1111/patient-1111.json | 0 .../measure100/2222/encounter-5678.json | 0 .../measure/measure100/2222/patient-2222.json | 0 .../measure200/1111/observation-1234.json | 0 .../measure/measure200/1111/patient-1111.json | 0 .../valueset/external/ValueSet-1234.json | 0 .../valueset/external/ValueSet-5678.json | 0 .../directoryPerType/prefixed/cql/Example.cql | 1 + .../resources/library/Library-123.json | 10 + .../resources/library/Library-456.json | 6 + .../resources/measure/Measure-XYZ.json | 4 + .../tests/condition/Condition-DEF.json | 12 + .../tests/condition/Condition-GHI.json | 12 + .../tests/condition/Condition-JKL.json | 12 + .../prefixed/tests/patient/Patient-ABC.json | 4 + .../vocabulary/valueset/ValueSet-456.json | 5 + .../directoryPerType/standard/cql/Example.cql | 1 + .../standard/resources/library/123.json | 10 + .../standard/resources/library/456.json | 6 + .../standard/resources/measure/XYZ.json | 4 + .../standard/tests/condition/DEF.json | 12 + .../standard/tests/condition/GHI.json | 12 + .../standard/tests/condition/JKL.json | 12 + .../standard/tests/patient/ABC.json | 4 + .../standard/vocabulary/valueset/456.json | 5 + .../resources/patient/patient-123.json | 0 .../resources/library/123.json | 6 + .../vocabulary/codesystem/ABC.json | 5 + .../vocabulary/codesystem/external/DEF.json | 5 + .../vocabulary/valueset/456.json | 5 + .../vocabulary/valueset/external/789.json | 5 + .../ig/standard/flat/Condition-DEF.json | 12 + .../ig/standard/flat/Condition-GHI.json | 12 + .../ig/standard/flat/Condition-JKL.json | 12 + .../ig/standard/flat/Library-123.json | 6 + .../ig/standard/flat/Library-456.json | 6 + .../ig/standard/flat/Measure-XYZ.json | 4 + .../ig/standard/flat/Patient-ABC.json | 4 + .../ig/standard/flat/ValueSet-456.json | 5 + .../ig/standard/flatNoTypeNames/123.json | 6 + .../ig/standard/flatNoTypeNames/456.json | 6 + .../ig/standard/flatNoTypeNames/789.json | 5 + .../ig/standard/flatNoTypeNames/ABC.json | 4 + .../ig/standard/flatNoTypeNames/DEF.json | 12 + .../ig/standard/flatNoTypeNames/GHI.json | 12 + .../ig/standard/flatNoTypeNames/JKL.json | 12 + .../ig/standard/flatNoTypeNames/XYZ.json | 4 + .../patient/measure-strat1-denom.json | 4 + .../mixedEncoding/resources/library/123.json | 6 + .../mixedEncoding/tests/patient/123.xml | 4 + .../vocabulary/codesystem/ABC.xml | 5 + .../vocabulary/codesystem/external/DEF.json | 5 + .../vocabulary/valueset/456.json | 5 + .../vocabulary/valueset/external/789.xml | 5 + .../noTestData/resources/library/123.json | 6 + .../noTestData/vocabulary/codesystem/ABC.json | 5 + .../noTestData/vocabulary/valueset/456.json | 5 + .../resources/patient/denom-patient-123.json | 4 + 89 files changed, 2030 insertions(+), 24 deletions(-) create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/BadDataTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CompartmentTest.java rename ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/{CqlIgStandardContentTest.java => CqlContentTest.java} (75%) create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ExternalTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatNoTypeNamesTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MixedEncodingTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/OverwriteEncodingTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/PrefixTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/XmlWriteTest.java create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/InvalidContent.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/MissingId.json rename ls/server/src/test/resources/{sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure100.cql => sampleIgs/ig/standard/badData/tests/patient/NoContent.json} (100%) create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/NotAFhirFile create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongId.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongResourceType.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/WrongVersion.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/compartment/resources/library/123.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/compartment/resources/library/456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/123/encounter/ABC.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/123/patient/123.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/456/encounter/DEF.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/compartment/tests/patient/456/patient/456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/compartment/vocabulary/valueset/456.json rename ls/server/src/test/resources/{cqlContentTest => sampleIgs/ig/standard/cqlContent}/cql/Test.cql (100%) rename ls/server/src/test/resources/{sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure200.cql => sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/cql/measure100.cql} (100%) create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/cql/measure200.cql rename ls/server/src/test/resources/{sample-igs/ig/standard/cql-measures/multi-measure => sampleIgs/ig/standard/cqlMeasures/multiMeasure}/input/tests/measure/measure100/1111/encounter-1234.json (100%) rename ls/server/src/test/resources/{sample-igs/ig/standard/cql-measures/multi-measure => sampleIgs/ig/standard/cqlMeasures/multiMeasure}/input/tests/measure/measure100/1111/patient-1111.json (100%) rename ls/server/src/test/resources/{sample-igs/ig/standard/cql-measures/multi-measure => sampleIgs/ig/standard/cqlMeasures/multiMeasure}/input/tests/measure/measure100/2222/encounter-5678.json (100%) rename ls/server/src/test/resources/{sample-igs/ig/standard/cql-measures/multi-measure => sampleIgs/ig/standard/cqlMeasures/multiMeasure}/input/tests/measure/measure100/2222/patient-2222.json (100%) rename ls/server/src/test/resources/{sample-igs/ig/standard/cql-measures/multi-measure => sampleIgs/ig/standard/cqlMeasures/multiMeasure}/input/tests/measure/measure200/1111/observation-1234.json (100%) rename ls/server/src/test/resources/{sample-igs/ig/standard/cql-measures/multi-measure => sampleIgs/ig/standard/cqlMeasures/multiMeasure}/input/tests/measure/measure200/1111/patient-1111.json (100%) rename ls/server/src/test/resources/{sample-igs/ig/standard/cql-measures/multi-measure => sampleIgs/ig/standard/cqlMeasures/multiMeasure}/input/vocabulary/valueset/external/ValueSet-1234.json (100%) rename ls/server/src/test/resources/{sample-igs/ig/standard/cql-measures/multi-measure => sampleIgs/ig/standard/cqlMeasures/multiMeasure}/input/vocabulary/valueset/external/ValueSet-5678.json (100%) create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/cql/Example.cql create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/library/Library-123.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/library/Library-456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/resources/measure/Measure-XYZ.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-DEF.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-GHI.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/condition/Condition-JKL.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/tests/patient/Patient-ABC.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/prefixed/vocabulary/valueset/ValueSet-456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/cql/Example.cql create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/library/123.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/library/456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/resources/measure/XYZ.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/DEF.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/GHI.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/condition/JKL.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/tests/patient/ABC.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/directoryPerType/standard/vocabulary/valueset/456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/emptyContent/resources/patient/patient-123.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/resources/library/123.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/codesystem/ABC.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/codesystem/external/DEF.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/valueset/456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/externalResource/vocabulary/valueset/external/789.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-DEF.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-GHI.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flat/Condition-JKL.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flat/Library-123.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flat/Library-456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flat/Measure-XYZ.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flat/Patient-ABC.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flat/ValueSet-456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/123.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/789.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/ABC.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/DEF.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/GHI.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/JKL.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/flatNoTypeNames/XYZ.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/misleadingFileName/resources/patient/measure-strat1-denom.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/resources/library/123.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/tests/patient/123.xml create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/codesystem/ABC.xml create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/codesystem/external/DEF.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/valueset/456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/mixedEncoding/vocabulary/valueset/external/789.xml create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/resources/library/123.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/vocabulary/codesystem/ABC.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/noTestData/vocabulary/valueset/456.json create mode 100644 ls/server/src/test/resources/sampleIgs/ig/standard/nonFhirFileName/resources/patient/denom-patient-123.json 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/CqlIgStandardContentTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.java similarity index 75% rename from ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlIgStandardContentTest.java rename to ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.java index 92880d25..004af4a6 100644 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlIgStandardContentTest.java +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.java @@ -1,6 +1,11 @@ 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; @@ -8,13 +13,7 @@ import org.junit.jupiter.api.io.TempDir; import org.opencds.cqf.fhir.test.Resources; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Path; - -import static org.junit.jupiter.api.Assertions.*; - -class CqlIgStandardContentTest { +class CqlContentTest { @TempDir static Path tempDir; @@ -23,7 +22,7 @@ class CqlIgStandardContentTest { 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("/cqlContentTest", tempDir); + Resources.copyFromJar("/sampleIgs/ig/standard/cqlContent", tempDir); } @Test @@ -59,37 +58,27 @@ void emptyLibraryDoesNothing() { @Test void nonLibraryResourceDoesNotThrow() { - assertDoesNotThrow(() -> { - IgStandardCqlContent.loadCqlContent(new ValueSet(), tempDir); - }); + 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); - }); + 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); - }); + assertThrows(ResourceNotFoundException.class, () -> IgStandardCqlContent.loadCqlContent(lib, tempDir)); } @Test void nullThrows() { - assertThrows(NullPointerException.class, () -> { - IgStandardCqlContent.loadCqlContent(null, tempDir); - }); + assertThrows(NullPointerException.class, () -> IgStandardCqlContent.loadCqlContent(null, tempDir)); var lib = new Library(); - assertThrows(NullPointerException.class, () -> { - IgStandardCqlContent.loadCqlContent(lib, null); - }); + 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 index 8e5e8a77..d458ab6f 100644 --- 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 @@ -24,7 +24,7 @@ public class MultiMeasureTest { private static final Logger log = LoggerFactory.getLogger(MultiMeasureTest.class); - private static final String rootDir = "/sample-igs/ig/standard/cql-measures/multi-measure"; + 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"; 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/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/sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure100.cql b/ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/NoContent.json similarity index 100% rename from ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure100.cql rename to ls/server/src/test/resources/sampleIgs/ig/standard/badData/tests/patient/NoContent.json 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/cqlContentTest/cql/Test.cql b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlContent/cql/Test.cql similarity index 100% rename from ls/server/src/test/resources/cqlContentTest/cql/Test.cql rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlContent/cql/Test.cql diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure200.cql b/ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/cql/measure100.cql similarity index 100% rename from ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/cql/measure200.cql rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/cql/measure100.cql 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/sample-igs/ig/standard/cql-measures/multi-measure/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 similarity index 100% rename from ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/1111/encounter-1234.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/encounter-1234.json diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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 similarity index 100% rename from ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/1111/patient-1111.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/patient-1111.json diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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 similarity index 100% rename from ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/2222/encounter-5678.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/encounter-5678.json diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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 similarity index 100% rename from ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure100/2222/patient-2222.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/patient-2222.json diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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 similarity index 100% rename from ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure200/1111/observation-1234.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/observation-1234.json diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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 similarity index 100% rename from ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/tests/measure/measure200/1111/patient-1111.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/patient-1111.json diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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 similarity index 100% rename from ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/vocabulary/valueset/external/ValueSet-1234.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/vocabulary/valueset/external/ValueSet-1234.json diff --git a/ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/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 similarity index 100% rename from ls/server/src/test/resources/sample-igs/ig/standard/cql-measures/multi-measure/input/vocabulary/valueset/external/ValueSet-5678.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/vocabulary/valueset/external/ValueSet-5678.json 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 From ae4c244c97c1970e92f3ad9cdf98fb95f196adb5 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Sat, 20 Dec 2025 10:14:27 -0700 Subject: [PATCH 20/30] disables failing github test --- .../cql/ls/server/repository/ig/standard/MultiMeasureTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index d458ab6f..c457d712 100644 --- 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 @@ -14,6 +14,7 @@ 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.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.opencds.cqf.fhir.test.Resources; @@ -99,7 +100,7 @@ void should_throwException_when_libraryDoesNotExist() { } // Test works locally but doesn't work on github - // @Disabled("Disabled until issue with running test on github is resolved.") + @Disabled("Disabled until issue with running test on github is resolved.") @Test void should_findResourceInCorrectRepo_when_resourcesIsolatedByRepo() { var id = Ids.newId(Patient.class, "1111"); From a9064cfa9f1fe02dd2ea0e2f72e423deb8aeffb8 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Sat, 20 Dec 2025 15:32:36 -0700 Subject: [PATCH 21/30] debugging failing github test --- .../ig/standard/IgStandardRepository.java | 77 ++++++++++++------- .../ig/standard/MultiMeasureTest.java | 2 +- 2 files changed, 50 insertions(+), 29 deletions(-) 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 index 6fa41db6..c6d3bd5a 100644 --- 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 @@ -42,6 +42,8 @@ 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 @@ -88,6 +90,8 @@ * */ 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; @@ -99,7 +103,7 @@ public class IgStandardRepository implements IRepository { CacheBuilder.newBuilder().concurrencyLevel(10).maximumSize(500).build(); // Metadata fields attached to resources that are read from the repository - // This fields are used to determine if a resource is external, and to + // 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 @@ -226,29 +230,35 @@ protected Path preferredPathForReso * @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 + * @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); + var potentialPaths = new ArrayList(); - // 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); - } + // Check the preferred path first + potentialPaths.add(this.preferredPathForResource(resourceType, id, igRepositoryCompartment)); - var potentialPaths = new ArrayList(); + // Add paths for external resources if applicable + var category = IgStandardResourceCategory.forType(resourceType.getSimpleName()); + if (category == IgStandardResourceCategory.TERMINOLOGY) { + var terminologyDirectory = this.root.resolve(CATEGORY_DIRECTORIES.get(category)); + var externalDirectory = terminologyDirectory.resolve(EXTERNAL_DIRECTORY); + for (var encoding : FILE_EXTENSIONS.keySet()) { + potentialPaths.add(externalDirectory.resolve( + fileNameForResource(resourceType.getSimpleName(), id.getIdPart(), encoding))); + } + } - for (var dir : potentialDirectories) { + // Also check paths without compartment if a compartment was specified, + // as resources might exist outside the compartment directory. + if (!igRepositoryCompartment.isEmpty()) { + var directoryWithoutCompartment = directoryForResource(resourceType, new IgStandardRepositoryCompartment()); for (var encoding : FILE_EXTENSIONS.keySet()) { - potentialPaths.add( - dir.resolve(fileNameForResource(resourceType.getSimpleName(), id.getIdPart(), encoding))); + potentialPaths.add(directoryWithoutCompartment.resolve( + fileNameForResource(resourceType.getSimpleName(), id.getIdPart(), encoding))); } } @@ -294,14 +304,18 @@ protected Path directoryForCategory( var category = IgStandardResourceCategory.forType(resourceType.getSimpleName()); var directory = CATEGORY_DIRECTORIES.get(category); - var path = root.resolve(directory); - if (category == IgStandardResourceCategory.DATA - && this.conventions.compartmentLayout() - == IgStandardConventions.CompartmentLayout.DIRECTORY_PER_COMPARTMENT) { - path = path.resolve(pathForCompartment(igStandardRepositoryCompartment)); + 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 path; + return categoryPath; } /** @@ -346,13 +360,16 @@ protected Path directoryForResource( */ // @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; } @@ -361,10 +378,10 @@ protected IBaseResource readResource(Path path) { 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; @@ -378,10 +395,12 @@ protected IBaseResource readResource(Path path) { protected IBaseResource cachedReadResource(Path path) { var o = this.resourceCache.getIfPresent(path); if (o != null) { + log.info("IgStandardRepository.readResource - Returning cached resource: {}", o); return o; } else { var resource = readResource(path); this.resourceCache.put(path, resource); + log.info("IgStandardRepository.readResource - Returning freshly loaded resource: {}", resource); return resource; } } @@ -505,11 +524,9 @@ public FhirContext 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. * *

@@ -535,21 +552,26 @@ 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("headers: {}", headers); var compartment = compartmentFrom(headers); var paths = this.potentialPathsForResource(resourceType, id, compartment); for (var path : paths) { + log.info("potentialPathsForResource path: {}", path); if (!path.toFile().exists()) { 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); } @@ -880,13 +902,12 @@ protected IgStandardRepositoryCompartment compartmentFrom(Map he : new IgStandardRepositoryCompartment(compartmentHeader); } - // Patient context is a special-case. We don't tack the compartment context on - // the end of the path. We just use the id as the directory. protected String pathForCompartment(IgStandardRepositoryCompartment igStandardRepositoryCompartment) { if (igStandardRepositoryCompartment.isEmpty()) { return ""; } - - return igStandardRepositoryCompartment.getType() + "/" + igStandardRepositoryCompartment.getId(); + // 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/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 index c457d712..bada2fc1 100644 --- 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 @@ -99,7 +99,7 @@ void should_throwException_when_libraryDoesNotExist() { assertThrows(ResourceNotFoundException.class, () -> terminologyRepo.read(Library.class, id)); } - // Test works locally but doesn't work on github + // 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() { From 2f860a869824c7f134f57f2a7752e6d96682418d Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Sat, 20 Dec 2025 15:37:54 -0700 Subject: [PATCH 22/30] debugging failing github test --- .../repository/ig/standard/IgStandardRepository.java | 8 ++++---- .../server/repository/ig/standard/MultiMeasureTest.java | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) 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 index c6d3bd5a..1bd1e3fa 100644 --- 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 @@ -395,12 +395,12 @@ protected IBaseResource readResource(Path path) { protected IBaseResource cachedReadResource(Path path) { var o = this.resourceCache.getIfPresent(path); if (o != null) { - log.info("IgStandardRepository.readResource - Returning cached resource: {}", o); + log.info("IgStandardRepository.cachedReadResource - Returning cached resource: {}", o); return o; } else { var resource = readResource(path); this.resourceCache.put(path, resource); - log.info("IgStandardRepository.readResource - Returning freshly loaded resource: {}", resource); + log.info("IgStandardRepository.cachedReadResource - Returning freshly loaded resource: {}", resource); return resource; } } @@ -554,12 +554,12 @@ public T read( requireNonNull(id, "id cannot be null"); log.info("IgStandardRepository.read - Attempting to read resource [{}].", id); - log.info("headers: {}", headers); + log.info("IgStandardRepository.read - headers: {}", headers); var compartment = compartmentFrom(headers); var paths = this.potentialPathsForResource(resourceType, id, compartment); for (var path : paths) { - log.info("potentialPathsForResource path: {}", path); + log.info("IgStandardRepository.read - potentialPathsForResource path: {}", path); if (!path.toFile().exists()) { continue; } 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 index bada2fc1..e1c5fdc7 100644 --- 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 @@ -14,7 +14,6 @@ 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.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.opencds.cqf.fhir.test.Resources; @@ -100,7 +99,7 @@ void should_throwException_when_libraryDoesNotExist() { } // Test works locally but doesn't work on GitHub - @Disabled("Disabled until issue with running test on github is resolved.") + // @Disabled("Disabled until issue with running test on github is resolved.") @Test void should_findResourceInCorrectRepo_when_resourcesIsolatedByRepo() { var id = Ids.newId(Patient.class, "1111"); From 0c2ea370f7f888da506ed7ae551d8c2b7eb42053 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Sat, 20 Dec 2025 16:15:30 -0700 Subject: [PATCH 23/30] debugging failing github test --- .../ig/standard/IgStandardConventions.java | 17 +------- .../ig/standard/IgStandardRepository.java | 39 +++++++------------ 2 files changed, 17 insertions(+), 39 deletions(-) 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 index a0c4a4b2..7d46c7e7 100644 --- 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 @@ -15,8 +15,8 @@ /** * This class represents the different file structures for an IG repository. The main differences - * between the various configurations are whether or not the files are organized by resource type - * and/or category, and whether or not the files are prefixed with the resource type. + * 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, @@ -26,34 +26,21 @@ public record IgStandardConventions( private static final Logger logger = LoggerFactory.getLogger(IgStandardConventions.class); - /** - * Whether or not the files are organized by resource type. - */ public enum FhirTypeLayout { DIRECTORY_PER_TYPE, FLAT } - /** - * Whether or not the resources are organized by category (tests, resources, vocabulary) - */ public enum CategoryLayout { DIRECTORY_PER_CATEGORY, FLAT } - /** - * Whether or not the files are organized by compartment. This is primarily used for tests to - * provide isolation between test cases. - */ public enum CompartmentLayout { DIRECTORY_PER_COMPARTMENT, FLAT } - /** - * Whether or not the files are prefixed with the resource type. - */ public enum FilenameMode { TYPE_AND_ID, ID_ONLY 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 index 1bd1e3fa..a9dcc3e3 100644 --- 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 @@ -110,7 +110,7 @@ public class IgStandardRepository implements IRepository { // Directory names static final String EXTERNAL_DIRECTORY = "external"; static final Map CATEGORY_DIRECTORIES = new ImmutableMap.Builder< - IgStandardResourceCategory, String>() + IgStandardResourceCategory, String>() .put(IgStandardResourceCategory.CONTENT, "resources") .put(IgStandardResourceCategory.DATA, "tests") .put(IgStandardResourceCategory.TERMINOLOGY, "vocabulary") @@ -236,29 +236,22 @@ protected Path preferredPathForReso protected List potentialPathsForResource( Class resourceType, I id, IgStandardRepositoryCompartment igRepositoryCompartment) { - var potentialPaths = new ArrayList(); - - // Check the preferred path first - potentialPaths.add(this.preferredPathForResource(resourceType, id, igRepositoryCompartment)); + var potentialDirectories = new ArrayList(); + var directory = directoryForResource(resourceType, igRepositoryCompartment); + potentialDirectories.add(directory); - // Add paths for external resources if applicable - var category = IgStandardResourceCategory.forType(resourceType.getSimpleName()); - if (category == IgStandardResourceCategory.TERMINOLOGY) { - var terminologyDirectory = this.root.resolve(CATEGORY_DIRECTORIES.get(category)); - var externalDirectory = terminologyDirectory.resolve(EXTERNAL_DIRECTORY); - for (var encoding : FILE_EXTENSIONS.keySet()) { - potentialPaths.add(externalDirectory.resolve( - fileNameForResource(resourceType.getSimpleName(), id.getIdPart(), encoding))); - } + // 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); } - // Also check paths without compartment if a compartment was specified, - // as resources might exist outside the compartment directory. - if (!igRepositoryCompartment.isEmpty()) { - var directoryWithoutCompartment = directoryForResource(resourceType, new IgStandardRepositoryCompartment()); + var potentialPaths = new ArrayList(); + + for (var dir : potentialDirectories) { for (var encoding : FILE_EXTENSIONS.keySet()) { - potentialPaths.add(directoryWithoutCompartment.resolve( - fileNameForResource(resourceType.getSimpleName(), id.getIdPart(), encoding))); + potentialPaths.add( + dir.resolve(fileNameForResource(resourceType.getSimpleName(), id.getIdPart(), encoding))); } } @@ -320,7 +313,6 @@ protected Path directoryForCategory( /** * 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 @@ -469,7 +461,6 @@ private boolean acceptByFileExtensionAndPrefix(Path path, String prefix) { /** * 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/") @@ -691,7 +682,7 @@ public MethodOutcome update(T resource, Map Date: Sat, 20 Dec 2025 16:27:59 -0700 Subject: [PATCH 24/30] debugging failing github test --- .../repository/ig/standard/IgStandardRepository.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 index a9dcc3e3..ef1406b8 100644 --- 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 @@ -110,7 +110,7 @@ public class IgStandardRepository implements IRepository { // Directory names static final String EXTERNAL_DIRECTORY = "external"; static final Map CATEGORY_DIRECTORIES = new ImmutableMap.Builder< - IgStandardResourceCategory, String>() + IgStandardResourceCategory, String>() .put(IgStandardResourceCategory.CONTENT, "resources") .put(IgStandardResourceCategory.DATA, "tests") .put(IgStandardResourceCategory.TERMINOLOGY, "vocabulary") @@ -241,7 +241,8 @@ protected List potentialPaths potentialDirectories.add(directory); // Currently, only terminology resources are allowed to be external - if (IgStandardResourceCategory.forType(resourceType.getSimpleName()) == IgStandardResourceCategory.TERMINOLOGY) { + if (IgStandardResourceCategory.forType(resourceType.getSimpleName()) + == IgStandardResourceCategory.TERMINOLOGY) { var externalDirectory = directory.resolve(EXTERNAL_DIRECTORY); potentialDirectories.add(externalDirectory); } @@ -551,7 +552,8 @@ public T read( var paths = this.potentialPathsForResource(resourceType, id, compartment); for (var path : paths) { log.info("IgStandardRepository.read - potentialPathsForResource path: {}", path); - if (!path.toFile().exists()) { + if (!Files.exists(path)) { // if (!path.toFile().exists()) { + log.info("IgStandardRepository.read - File doesn't exist at [{}]. Continuing loop.", path); continue; } @@ -682,7 +684,7 @@ public MethodOutcome update(T resource, Map Date: Sun, 21 Dec 2025 12:12:32 -0700 Subject: [PATCH 25/30] renames test resource files --- .../measure100/1111/{encounter-1234.json => Encounter-1234.json} | 0 .../measure100/1111/{patient-1111.json => Patient-1111.json} | 0 .../measure100/2222/{encounter-5678.json => Encounter-5678.json} | 0 .../measure100/2222/{patient-2222.json => Patient-2222.json} | 0 .../1111/{observation-1234.json => Observation-1234.json} | 0 .../measure200/1111/{patient-1111.json => Patient-1111.json} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/{encounter-1234.json => Encounter-1234.json} (100%) rename ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/{patient-1111.json => Patient-1111.json} (100%) rename ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/{encounter-5678.json => Encounter-5678.json} (100%) rename ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/{patient-2222.json => Patient-2222.json} (100%) rename ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/{observation-1234.json => Observation-1234.json} (100%) rename ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/{patient-1111.json => Patient-1111.json} (100%) 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 similarity index 100% rename from ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/encounter-1234.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/Encounter-1234.json 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 similarity index 100% rename from ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/patient-1111.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/1111/Patient-1111.json 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 similarity index 100% rename from ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/encounter-5678.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/Encounter-5678.json 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 similarity index 100% rename from ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/patient-2222.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure100/2222/Patient-2222.json 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 similarity index 100% rename from ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/observation-1234.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/Observation-1234.json 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 similarity index 100% rename from ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/patient-1111.json rename to ls/server/src/test/resources/sampleIgs/ig/standard/cqlMeasures/multiMeasure/input/tests/measure/measure200/1111/Patient-1111.json From 86eae6749637a0e069e20d437812a14830760ceb Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Tue, 6 Jan 2026 13:34:45 -0500 Subject: [PATCH 26/30] adds more test coverage --- .../cqf/cql/ls/server/command/CqlCommand.java | 16 ++-- .../ls/server/service/FileContentService.java | 2 +- .../provider/ContentServiceProviderTest.java | 36 +++++++++ .../ig/standard/ConventionsTest.java | 81 +++++++++++++++++++ .../content/sample-library-1.0.0.json | 16 ++++ .../content/sample-library-2.0.0.json | 16 ++++ 6 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.java create mode 100644 ls/server/src/test/resources/provider/content/sample-library-1.0.0.json create mode 100644 ls/server/src/test/resources/provider/content/sample-library-2.0.0.json 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 91d55832..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 @@ -151,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 diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.java index 1a56996e..9413086e 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.java @@ -31,7 +31,7 @@ // And further that will always be of the form [.[.]] // Usage outside these boundaries will result in errors or incorrect behavior. public class FileContentService implements ContentService { - private static final Logger log = LoggerFactory.getLogger(FileContentService.class); + private static final Logger log = LoggerFactory.getLogger( FileContentService.class); protected final List workspaceFolders; 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..9988bad6 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.java @@ -0,0 +1,36 @@ +package org.opencds.cqf.cql.ls.server.provider; + +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; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +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/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..6bc084e1 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.java @@ -0,0 +1,81 @@ +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/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 From 90cdcd1166d98304b9902a32ff3c8da5645dd047 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Tue, 6 Jan 2026 13:38:02 -0500 Subject: [PATCH 27/30] spotless applied --- .../ls/server/service/FileContentService.java | 2 +- .../provider/ContentServiceProviderTest.java | 19 +++++++------------ .../ig/standard/ConventionsTest.java | 15 +++++++++++---- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.java index 9413086e..1a56996e 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.java +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.java @@ -31,7 +31,7 @@ // And further that will always be of the form [.[.]] // Usage outside these boundaries will result in errors or incorrect behavior. public class FileContentService implements ContentService { - private static final Logger log = LoggerFactory.getLogger( FileContentService.class); + private static final Logger log = LoggerFactory.getLogger(FileContentService.class); protected final List workspaceFolders; 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 index 9988bad6..612be057 100644 --- 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 @@ -1,16 +1,15 @@ package org.opencds.cqf.cql.ls.server.provider; -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; +import static org.junit.jupiter.api.Assertions.*; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; import java.util.Set; - -import static org.junit.jupiter.api.Assertions.*; +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 { @@ -20,17 +19,13 @@ void should_throwException_when_gettingLibrary() throws Exception { versionedIdentifier.withVersion("1.0.0"); ContentServiceSourceProvider contentServiceSourceProvider = new ContentServiceSourceProvider( - Uris.parseOrNull("/provider/content/sample-library-1.0.0.json"), - new ContentService() { + 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)); + assertThrows(RuntimeException.class, () -> contentServiceSourceProvider.getLibrarySource(versionedIdentifier)); } } - 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 index 6bc084e1..aa6e14b3 100644 --- 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 @@ -1,4 +1,5 @@ package org.opencds.cqf.cql.ls.server.repository.ig.standard; + import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; @@ -24,12 +25,15 @@ static void setup() throws URISyntaxException, IOException, ClassNotFoundExcepti @Test void autoDetectDefault() { assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(null)); - assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("does_not_exist"))); + assertEquals( + IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("does_not_exist"))); } @Test void autoDetectStandard() { - assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("directoryPerType/standard"))); + assertEquals( + IgStandardConventions.STANDARD, + IgStandardConventions.autoDetect(tempDir.resolve("directoryPerType/standard"))); } @Test @@ -57,7 +61,9 @@ void autoDetectFlatNoTypeNames() { @Test void autoDetectWithMisleadingFileName() { - assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("misleadingFileName"))); + assertEquals( + IgStandardConventions.STANDARD, + IgStandardConventions.autoDetect(tempDir.resolve("misleadingFileName"))); } @Test @@ -67,7 +73,8 @@ void autoDetectWithEmptyContent() { @Test void autoDetectWithNonFhirFilename() { - assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("nonFhirFilename"))); + assertEquals( + IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("nonFhirFilename"))); } @Test From 168156d3240b7b425281e4f6d0e4189886865d16 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Tue, 6 Jan 2026 14:12:06 -0500 Subject: [PATCH 28/30] adds instantiation test --- .../cqf/cql/ls/core/utility/ConvertersTest.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 index 1b7e8a86..0ec444a7 100644 --- 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 @@ -12,13 +12,19 @@ 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(actual, expected); + assertEquals(expected, actual); } catch (IOException e) { fail("Unexpected exception thrown. {}", e); } @@ -26,11 +32,15 @@ void should_returnString_when_inputStreamExists() { @Test void should_returnStringWithLineBreaks_when_inputStreamHasLineBreaksExists() { - var expected = "the first day in spring –\n" + "a wind from the ocean\n" + "but no ocean in sight"; + 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(actual, expected); + assertEquals(expected, actual); } catch (IOException e) { fail("Unexpected exception thrown. {}", e); } From 070e81250c8d3a3d53a86c75d1bbf85f7371b507 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Tue, 6 Jan 2026 16:09:34 -0500 Subject: [PATCH 29/30] bump version to 4.0.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a33f1c25..7cede42f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.opencds.cqf.cql.ls cql-ls pom - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT CQL Language Server A Language Server for CQL implementing the LSP From 726708dcd0feaa7dbb78a10f02b1fc51bab2701d Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Tue, 6 Jan 2026 16:17:55 -0500 Subject: [PATCH 30/30] bump version to 4.0.0-SNAPSHOT --- core/pom.xml | 2 +- debug/server/pom.xml | 2 +- debug/service/pom.xml | 2 +- ls/server/pom.xml | 2 +- ls/service/pom.xml | 2 +- plugin/debug/pom.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 4abb1937..2e3ecab0 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -12,7 +12,7 @@ org.opencds.cqf.cql.ls cql-ls - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT ../pom.xml diff --git a/debug/server/pom.xml b/debug/server/pom.xml index a5bcb616..8b55c5f0 100644 --- a/debug/server/pom.xml +++ b/debug/server/pom.xml @@ -12,7 +12,7 @@ org.opencds.cqf.cql.ls cql-ls - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT ../../pom.xml diff --git a/debug/service/pom.xml b/debug/service/pom.xml index 1f5aafaf..147f9135 100644 --- a/debug/service/pom.xml +++ b/debug/service/pom.xml @@ -12,7 +12,7 @@ org.opencds.cqf.cql.ls cql-ls - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT ../../pom.xml diff --git a/ls/server/pom.xml b/ls/server/pom.xml index 197ff073..95c3dcc6 100644 --- a/ls/server/pom.xml +++ b/ls/server/pom.xml @@ -12,7 +12,7 @@ org.opencds.cqf.cql.ls cql-ls - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT ../../pom.xml diff --git a/ls/service/pom.xml b/ls/service/pom.xml index 0202f6bd..7629bd30 100644 --- a/ls/service/pom.xml +++ b/ls/service/pom.xml @@ -11,7 +11,7 @@ org.opencds.cqf.cql.ls cql-ls - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT ../../pom.xml diff --git a/plugin/debug/pom.xml b/plugin/debug/pom.xml index 31bec502..2ebaf17c 100644 --- a/plugin/debug/pom.xml +++ b/plugin/debug/pom.xml @@ -11,7 +11,7 @@ org.opencds.cqf.cql.ls cql-ls - 3.9.0-SNAPSHOT + 4.0.0-SNAPSHOT ../../pom.xml