From c0f528d14ad9577c97885bf482343f94593e5f0d Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 16 Mar 2026 08:58:02 -0400 Subject: [PATCH 01/19] migration to kotlin --- .../cqf/cql/ls/core/ContentService.java | 49 - .../opencds/cqf/cql/ls/core/ContentService.kt | 39 + .../cqf/cql/ls/core/utility/Converters.java | 36 - .../cqf/cql/ls/core/utility/Converters.kt | 39 + .../opencds/cqf/cql/ls/core/utility/Uris.java | 104 -- .../opencds/cqf/cql/ls/core/utility/Uris.kt | 76 ++ .../cql/ls/core/utility/ConvertersTest.java | 79 -- .../cqf/cql/ls/core/utility/ConvertersTest.kt | 63 ++ .../cqf/cql/ls/core/utility/UrisTest.java | 146 --- .../cqf/cql/ls/core/utility/UrisTest.kt | 138 +++ .../opencds/cqf/cql/debug/CqlDebugServer.java | 108 --- .../opencds/cqf/cql/debug/CqlDebugServer.kt | 97 ++ .../cql/debug/IDebugProtocolClientAware.java | 7 - .../cql/debug/IDebugProtocolClientAware.kt | 7 + .../{ServerState.java => ServerState.kt} | 4 +- .../cqf/cql/debug/DebugServerTest.java | 72 -- .../opencds/cqf/cql/debug/DebugServerTest.kt | 69 ++ debug/service/pom.xml | 45 +- .../opencds/cqf/cql/debug/service/Main.java | 47 - .../org/opencds/cqf/cql/debug/service/main.kt | 28 + ls/server/pom.xml | 32 +- .../opencds/cqf/cql/ls/server/Constants.java | 10 - .../opencds/cqf/cql/ls/server/Constants.kt | 7 + .../cqf/cql/ls/server/CqlLanguageServer.java | 88 -- .../cqf/cql/ls/server/CqlLanguageServer.kt | 67 ++ .../cqf/cql/ls/server/command/CliCommand.java | 17 - .../cqf/cql/ls/server/command/CliCommand.kt | 9 + .../cqf/cql/ls/server/command/CqlCommand.java | 333 ------- .../cqf/cql/ls/server/command/CqlCommand.kt | 280 ++++++ .../command/DebugCqlCommandContribution.java | 80 -- .../command/DebugCqlCommandContribution.kt | 67 ++ .../cql/ls/server/command/NoOpRepository.java | 78 -- .../cql/ls/server/command/NoOpRepository.kt | 61 ++ .../command/ViewElmCommandContribution.java | 92 -- .../command/ViewElmCommandContribution.kt | 71 ++ .../cql/ls/server/config/PluginConfig.java | 59 -- .../cql/ls/server/config/ServerConfig.java | 140 --- .../cqf/cql/ls/server/event/BaseEvent.java | 14 - .../cqf/cql/ls/server/event/BaseEvent.kt | 5 + .../event/DidChangeTextDocumentEvent.java | 9 - .../event/DidChangeTextDocumentEvent.kt | 5 + .../event/DidChangeWatchedFilesEvent.java | 9 - .../event/DidChangeWatchedFilesEvent.kt | 5 + .../event/DidCloseTextDocumentEvent.java | 9 - .../server/event/DidCloseTextDocumentEvent.kt | 5 + .../event/DidOpenTextDocumentEvent.java | 9 - .../server/event/DidOpenTextDocumentEvent.kt | 5 + .../event/DidSaveTextDocumentEvent.java | 9 - .../server/event/DidSaveTextDocumentEvent.kt | 5 + .../manager/CompilerOptionsManager.java | 80 -- .../server/manager/CompilerOptionsManager.kt | 71 ++ .../server/manager/CqlCompilationManager.java | 96 -- .../server/manager/CqlCompilationManager.kt | 71 ++ .../ls/server/manager/IgContextManager.java | 129 --- .../cql/ls/server/manager/IgContextManager.kt | 101 ++ .../ls/server/plugin/CommandContribution.java | 16 - .../ls/server/plugin/CommandContribution.kt | 12 + .../plugin/CqlLanguageServerPlugin.java | 7 - .../server/plugin/CqlLanguageServerPlugin.kt | 7 + .../CqlLanguageServerPluginFactory.java | 15 - .../plugin/CqlLanguageServerPluginFactory.kt | 16 + .../ContentServiceModelInfoProvider.java | 50 - .../ContentServiceModelInfoProvider.kt | 37 + .../ContentServiceSourceProvider.java | 34 - .../provider/ContentServiceSourceProvider.kt | 19 + .../server/provider/FormattingProvider.java | 43 - .../ls/server/provider/FormattingProvider.kt | 36 + .../cql/ls/server/provider/HoverProvider.java | 116 --- .../cql/ls/server/provider/HoverProvider.kt | 94 ++ .../ig/standard/IgStandardConventions.java | 251 ----- .../ig/standard/IgStandardConventions.kt | 176 ++++ .../ig/standard/IgStandardCqlContent.java | 73 -- .../ig/standard/IgStandardCqlContent.kt | 59 ++ .../standard/IgStandardEncodingBehavior.java | 41 - .../ig/standard/IgStandardEncodingBehavior.kt | 31 + .../ig/standard/IgStandardRepository.java | 906 ------------------ .../ig/standard/IgStandardRepository.kt | 513 ++++++++++ .../IgStandardRepositoryCompartment.java | 84 -- .../IgStandardRepositoryCompartment.kt | 51 + .../standard/IgStandardResourceCategory.java | 24 - .../ig/standard/IgStandardResourceCategory.kt | 22 + .../server/service/ActiveContentService.java | 179 ---- .../ls/server/service/ActiveContentService.kt | 135 +++ .../service/CqlTextDocumentService.java | 102 -- .../server/service/CqlTextDocumentService.kt | 86 ++ .../server/service/CqlWorkspaceService.java | 202 ---- .../ls/server/service/CqlWorkspaceService.kt | 166 ++++ .../ls/server/service/DiagnosticsService.java | 221 ----- .../ls/server/service/DiagnosticsService.kt | 178 ++++ .../service/FederatedContentService.java | 43 - .../server/service/FederatedContentService.kt | 30 + .../ls/server/service/FileContentService.java | 194 ---- .../ls/server/service/FileContentService.kt | 123 +++ .../cql/ls/server/utility/Diagnostics.java | 63 -- .../cqf/cql/ls/server/utility/Diagnostics.kt | 52 + .../cqf/cql/ls/server/utility/Futures.java | 16 - .../cqf/cql/ls/server/utility/Futures.kt | 13 + .../visitor/ExpressionTrackBackVisitor.java | 79 -- .../visitor/ExpressionTrackBackVisitor.kt | 51 + .../cqf/cql/ls/server/LanguageServerTest.kt | 88 ++ .../ViewElmCommandContributionTest.java | 115 --- .../manager/CompilerOptionsManagerTest.java | 127 --- .../provider/ContentServiceProviderTest.java | 31 - .../ContentServiceSourceProviderTest.java | 51 - .../provider/FormattingProviderTest.java | 42 - .../ls/server/provider/HoverProviderTest.java | 94 -- .../repository/ig/standard/BadDataTest.java | 70 -- .../ig/standard/CompartmentTest.java | 243 ----- .../ig/standard/ConventionsTest.java | 88 -- .../ig/standard/CqlContentTest.java | 84 -- .../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 | 117 --- .../ig/standard/NoTestDataTest.java | 141 --- .../ig/standard/OverwriteEncodingTest.java | 62 -- .../repository/ig/standard/PrefixTest.java | 221 ----- .../repository/ig/standard/XmlWriteTest.java | 95 -- .../service/ActiveContentServiceTest.java | 188 ---- .../service/DiagnosticsServiceTest.java | 109 --- .../server/service/DiagnosticsServiceTest.kt | 108 +++ .../service/FederatedContentServiceTest.java | 91 -- .../service/FileContentServiceTest.java | 143 --- .../ls/server/service/TestContentService.java | 27 - .../ls/server/service/TestContentService.kt | 15 + .../ls/server/utility/DiagnosticsTest.java | 133 --- .../ExpressionTrackBackVisitorTest.java | 81 -- .../command/ViewElmCommandContributionTest.kt | 134 +++ .../manager/CompilerOptionsManagerTest.kt | 122 +++ .../provider/ContentServiceProviderTest.kt | 33 + .../ContentServiceSourceProviderTest.kt | 45 + .../server/provider/FormattingProviderTest.kt | 41 + .../ls/server/provider/HoverProviderTest.kt | 117 +++ .../repository/ig/standard/BadDataTest.kt | 76 ++ .../repository/ig/standard/CompartmentTest.kt | 260 +++++ .../repository/ig/standard/ConventionsTest.kt | 94 ++ .../repository/ig/standard/CqlContentTest.kt | 96 ++ .../repository/ig/standard/DirectoryTest.kt | 190 ++++ .../repository/ig/standard/ExternalTest.kt | 92 ++ .../ig/standard/FlatNoTypeNamesTest.kt | 190 ++++ .../server/repository/ig/standard/FlatTest.kt | 190 ++++ .../ig/standard/MixedEncodingTest.kt | 190 ++++ .../ig/standard/MultiMeasureTest.kt | 127 +++ .../repository/ig/standard/NoTestDataTest.kt | 153 +++ .../ig/standard/OverwriteEncodingTest.kt | 69 ++ .../repository/ig/standard/PrefixTest.kt | 239 +++++ .../repository/ig/standard/XmlWriteTest.kt | 103 ++ .../service/ActiveContentServiceTest.kt | 178 ++++ .../service/FederatedContentServiceTest.kt | 84 ++ .../server/service/FileContentServiceTest.kt | 132 +++ .../cql/ls/server/utility/DiagnosticsTest.kt | 130 +++ .../visitor/ExpressionTrackBackVisitorTest.kt | 82 ++ .../org.mockito.plugins.MockMaker | 1 + ls/service/pom.xml | 48 +- .../ls/service/LanguageClientAppender.java | 53 - .../cql/ls/service/LanguageClientAppender.kt | 26 + .../org/opencds/cqf/cql/ls/service/Main.java | 170 ---- .../org/opencds/cqf/cql/ls/service/main.kt | 114 +++ .../src/main/resources/application.properties | 4 - plugin/debug/pom.xml | 26 + .../debug/DebugCommandContribution.java | 52 - .../plugin/debug/DebugCommandContribution.kt | 49 + .../cqf/cql/ls/plugin/debug/DebugPlugin.java | 43 - .../cqf/cql/ls/plugin/debug/DebugPlugin.kt | 28 + .../ls/plugin/debug/DebugPluginFactory.java | 23 - .../cql/ls/plugin/debug/DebugPluginFactory.kt | 23 + .../ls/plugin/debug/session/DebugSession.java | 80 -- .../ls/plugin/debug/session/DebugSession.kt | 77 ++ .../plugin/debug/client/TestDebugClient.java | 39 - .../ls/plugin/debug/client/TestDebugClient.kt | 31 + .../debug/session/DebugSessionTest.java | 49 - .../plugin/debug/session/DebugSessionTest.kt | 47 + pom.xml | 60 +- 175 files changed, 7236 insertions(+), 8381 deletions(-) delete mode 100644 core/src/main/java/org/opencds/cqf/cql/ls/core/ContentService.java create mode 100644 core/src/main/java/org/opencds/cqf/cql/ls/core/ContentService.kt delete mode 100644 core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.java create mode 100644 core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.kt delete mode 100644 core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Uris.java create mode 100644 core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Uris.kt delete mode 100644 core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.java create mode 100644 core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.kt delete mode 100644 core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.java create mode 100644 core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt delete mode 100644 debug/server/src/main/java/org/opencds/cqf/cql/debug/CqlDebugServer.java create mode 100644 debug/server/src/main/java/org/opencds/cqf/cql/debug/CqlDebugServer.kt delete mode 100644 debug/server/src/main/java/org/opencds/cqf/cql/debug/IDebugProtocolClientAware.java create mode 100644 debug/server/src/main/java/org/opencds/cqf/cql/debug/IDebugProtocolClientAware.kt rename debug/server/src/main/java/org/opencds/cqf/cql/debug/{ServerState.java => ServerState.kt} (54%) delete mode 100644 debug/server/src/test/java/org/opencds/cqf/cql/debug/DebugServerTest.java create mode 100644 debug/server/src/test/java/org/opencds/cqf/cql/debug/DebugServerTest.kt delete mode 100644 debug/service/src/main/java/org/opencds/cqf/cql/debug/service/Main.java create mode 100644 debug/service/src/main/java/org/opencds/cqf/cql/debug/service/main.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/Constants.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/Constants.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CliCommand.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CliCommand.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/NoOpRepository.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/NoOpRepository.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/config/PluginConfig.java delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/config/ServerConfig.java delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/BaseEvent.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/BaseEvent.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeTextDocumentEvent.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeTextDocumentEvent.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeWatchedFilesEvent.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeWatchedFilesEvent.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidCloseTextDocumentEvent.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidCloseTextDocumentEvent.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidOpenTextDocumentEvent.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidOpenTextDocumentEvent.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidSaveTextDocumentEvent.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidSaveTextDocumentEvent.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManager.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManager.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CommandContribution.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CommandContribution.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPlugin.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPlugin.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPluginFactory.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPluginFactory.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProvider.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProvider.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/FormattingProvider.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/FormattingProvider.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/HoverProvider.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/HoverProvider.kt delete 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/IgStandardConventions.kt delete 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/IgStandardCqlContent.kt delete 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/IgStandardEncodingBehavior.kt delete 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/IgStandardRepository.kt delete 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/IgStandardRepositoryCompartment.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Futures.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Futures.kt delete mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.java create mode 100644 ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.kt create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/LanguageServerTest.kt delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContributionTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProviderTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/FormattingProviderTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/HoverProviderTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/BadDataTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CompartmentTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ExternalTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatNoTypeNamesTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MixedEncodingTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/OverwriteEncodingTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/PrefixTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/XmlWriteTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.kt delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/FileContentServiceTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/TestContentService.java create mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/TestContentService.kt delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/utility/DiagnosticsTest.java delete mode 100644 ls/server/src/test/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.java create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContributionTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProviderTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/FormattingProviderTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/HoverProviderTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/BadDataTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/CompartmentTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/ExternalTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatNoTypeNamesTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/MixedEncodingTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/OverwriteEncodingTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/PrefixTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/XmlWriteTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FileContentServiceTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/utility/DiagnosticsTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.kt create mode 100644 ls/server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker delete mode 100644 ls/service/src/main/java/org/opencds/cqf/cql/ls/service/LanguageClientAppender.java create mode 100644 ls/service/src/main/java/org/opencds/cqf/cql/ls/service/LanguageClientAppender.kt delete mode 100644 ls/service/src/main/java/org/opencds/cqf/cql/ls/service/Main.java create mode 100644 ls/service/src/main/java/org/opencds/cqf/cql/ls/service/main.kt delete mode 100644 ls/service/src/main/resources/application.properties delete mode 100644 plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContribution.java create mode 100644 plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContribution.kt delete mode 100644 plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPlugin.java create mode 100644 plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPlugin.kt delete mode 100644 plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPluginFactory.java create mode 100644 plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPluginFactory.kt delete mode 100644 plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSession.java create mode 100644 plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSession.kt delete mode 100644 plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/client/TestDebugClient.java create mode 100644 plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/client/TestDebugClient.kt delete mode 100644 plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSessionTest.java create mode 100644 plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSessionTest.kt diff --git a/core/src/main/java/org/opencds/cqf/cql/ls/core/ContentService.java b/core/src/main/java/org/opencds/cqf/cql/ls/core/ContentService.java deleted file mode 100644 index 4ed9f3ef..00000000 --- a/core/src/main/java/org/opencds/cqf/cql/ls/core/ContentService.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.opencds.cqf.cql.ls.core; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; -import org.apache.commons.lang3.NotImplementedException; -import org.hl7.elm.r1.VersionedIdentifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public interface ContentService { - static final Logger log = LoggerFactory.getLogger(ContentService.class); - - default Set locate(URI root, VersionedIdentifier identifier) { - throw new NotImplementedException(); - } - - default InputStream read(URI root, VersionedIdentifier identifier) { - Objects.requireNonNull(identifier); - - Set locations = locate(root, identifier); - if (locations.isEmpty()) { - return null; - } - - if (locations.size() > 1) { - String allLocations = - String.join("%n", locations.stream().map(String::valueOf).collect(Collectors.toList())); - throw new IllegalStateException(String.format( - "more than one location was found for library: %s version: %s in the current workspace:%n%s", - identifier.getId(), identifier.getVersion(), allLocations)); - } - return read(locations.iterator().next()); - } - - default InputStream read(URI uri) { - Objects.requireNonNull(uri); - - try { - return uri.toURL().openStream(); - } catch (IOException e) { - log.warn(String.format("error opening stream for: %s", uri.toString()), e); - return null; - } - } -} diff --git a/core/src/main/java/org/opencds/cqf/cql/ls/core/ContentService.kt b/core/src/main/java/org/opencds/cqf/cql/ls/core/ContentService.kt new file mode 100644 index 00000000..0f13ad10 --- /dev/null +++ b/core/src/main/java/org/opencds/cqf/cql/ls/core/ContentService.kt @@ -0,0 +1,39 @@ +package org.opencds.cqf.cql.ls.core + +import org.apache.commons.lang3.NotImplementedException +import org.hl7.elm.r1.VersionedIdentifier +import org.slf4j.LoggerFactory +import java.io.InputStream +import java.net.URI + +interface ContentService { + fun locate(root: URI, identifier: VersionedIdentifier): Set { + throw NotImplementedException() + } + + fun read(root: URI, identifier: VersionedIdentifier): InputStream? { + val locations = locate(root, identifier) + if (locations.isEmpty()) return null + + if (locations.size > 1) { + val allLocations = locations.joinToString("%n") { it.toString() } + throw IllegalStateException( + "more than one location was found for library: ${identifier.id} version: ${identifier.version} in the current workspace:%n$allLocations" + ) + } + return read(locations.first()) + } + + fun read(uri: URI): InputStream? { + return try { + uri.toURL().openStream() + } catch (e: Exception) { + log.warn("error opening stream for: $uri", e) + null + } + } + + companion object { + private val log = LoggerFactory.getLogger(ContentService::class.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 deleted file mode 100644 index 19f47637..00000000 --- a/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.opencds.cqf.cql.ls.core.utility; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import kotlinx.io.Buffer; -import kotlinx.io.Source; - -public class Converters { - - public static String inputStreamToString(InputStream inputStream) throws IOException { - StringBuilder resultStringBuilder = new StringBuilder(); - try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { - String line; - while ((line = br.readLine()) != null) { - if (!resultStringBuilder.isEmpty()) resultStringBuilder.append("\n"); // Append newline if needed - resultStringBuilder.append(line); - } - } - return resultStringBuilder.toString(); - } - - public static Source stringToSource(String text) { - Buffer buffer = new Buffer(); - // Write the string to the buffer using a specific character encoding - buffer.write(text.getBytes(), 0, text.length()); - // Return the buffer as a Source - return buffer; - } - - public static Source inputStreamToSource(InputStream inputStream) throws IOException { - return stringToSource(inputStreamToString(inputStream)); - } -} diff --git a/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.kt b/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.kt new file mode 100644 index 00000000..023acd7f --- /dev/null +++ b/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Converters.kt @@ -0,0 +1,39 @@ +package org.opencds.cqf.cql.ls.core.utility + +import kotlinx.io.Buffer +import kotlinx.io.Source +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets + +object Converters { + @JvmStatic + @Throws(IOException::class) + fun inputStreamToString(inputStream: InputStream): String { + val sb = StringBuilder() + BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8)).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + if (sb.isNotEmpty()) sb.append("\n") + sb.append(line) + } + } + return sb.toString() + } + + @JvmStatic + fun stringToSource(text: String): Source { + val buffer = Buffer() + val bytes = text.toByteArray() + buffer.write(bytes, 0, bytes.size) + return buffer + } + + @JvmStatic + @Throws(IOException::class) + fun inputStreamToSource(inputStream: InputStream): Source { + return stringToSource(inputStreamToString(inputStream)) + } +} diff --git a/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Uris.java b/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Uris.java deleted file mode 100644 index a2347643..00000000 --- a/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Uris.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.opencds.cqf.cql.ls.core.utility; - -import java.io.File; -import java.net.URI; -import org.apache.commons.lang3.SystemUtils; - -public class Uris { - private Uris() {} - - private static final String FILE_UNC_PREFIX = "file:////"; - private static final String FILE_SCHEME = "file"; - - public static URI getHead(URI uri) { - String path = uri.getRawPath(); - if (path != null) { - int index = path.lastIndexOf("/"); - if (index > -1) { - return withPath(uri, path.substring(0, index)); - } - - return uri; - } - - return uri; - } - - public static URI withPath(URI uri, String path) { - try { - return URI.create((uri.getScheme() != null ? uri.getScheme() + ":" : "") + "//" - + createAuthority(uri.getRawAuthority()) + createPath(path) - + createQuery(uri.getRawQuery()) + createFragment(uri.getRawFragment())); - } catch (Exception e) { - return null; - } - } - - public static URI addPath(URI uri, String path) { - return withPath(uri, stripTrailingSlash(uri.getRawPath()) + createPath(path)); - } - - public static URI parseOrNull(String uriString) { - try { - URI uri = new URI(uriString); - if (SystemUtils.IS_OS_WINDOWS && FILE_SCHEME.equals(uri.getScheme())) { - uri = new File(uri.getSchemeSpecificPart()).toURI(); - } - - return uri; - } catch (Exception e) { - return null; - } - } - - public static String toClientUri(URI uri) { - if (uri == null) { - return null; - } - - String uriString = uri.toString(); - if (SystemUtils.IS_OS_WINDOWS && uriString.startsWith(FILE_UNC_PREFIX)) { - uriString = uriString.replace(FILE_UNC_PREFIX, "file://"); - } - - return uriString; - } - - private static String createAuthority(String rawAuthority) { - return rawAuthority != null ? rawAuthority : ""; - } - - private static String stripTrailingSlash(String path) { - if (path == null || path.isEmpty()) { - return ""; - } - - if (path.endsWith("/")) { - return path.substring(0, path.length() - 1); - } - - return path; - } - - private static String createPath(String pathValue) { - return ensurePrefix("/", pathValue); - } - - private static String createQuery(String queryValue) { - return ensurePrefix("?", queryValue); - } - - private static String createFragment(String fragmentValue) { - return ensurePrefix("#", fragmentValue); - } - - private static String ensurePrefix(String prefix, String value) { - if (value == null || value.isEmpty()) { - return ""; - } else if (value.startsWith(prefix)) { - return value; - } else { - return prefix + value; - } - } -} diff --git a/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Uris.kt b/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Uris.kt new file mode 100644 index 00000000..82ef5028 --- /dev/null +++ b/core/src/main/java/org/opencds/cqf/cql/ls/core/utility/Uris.kt @@ -0,0 +1,76 @@ +package org.opencds.cqf.cql.ls.core.utility + +import org.apache.commons.lang3.SystemUtils +import java.io.File +import java.net.URI + +object Uris { + private const val FILE_UNC_PREFIX = "file:////" + private const val FILE_SCHEME = "file" + + @JvmStatic + fun getHead(uri: URI): URI { + val path = uri.rawPath ?: return uri + val index = path.lastIndexOf("/") + return if (index > -1) withPath(uri, path.substring(0, index)) ?: uri else uri + } + + @JvmStatic + fun withPath(uri: URI, path: String): URI? { + return try { + URI.create( + (if (uri.scheme != null) "${uri.scheme}:" else "") + + "//" + createAuthority(uri.rawAuthority) + createPath(path) + + createQuery(uri.rawQuery) + createFragment(uri.rawFragment) + ) + } catch (e: Exception) { + null + } + } + + @JvmStatic + fun addPath(uri: URI, path: String): URI? { + return withPath(uri, stripTrailingSlash(uri.rawPath) + createPath(path)) + } + + @JvmStatic + fun parseOrNull(uriString: String): URI? { + return try { + var uri = URI(uriString) + if (SystemUtils.IS_OS_WINDOWS && FILE_SCHEME == uri.scheme) { + uri = File(uri.schemeSpecificPart).toURI() + } + uri + } catch (e: Exception) { + null + } + } + + @JvmStatic + fun toClientUri(uri: URI?): String? { + if (uri == null) return null + var uriString = uri.toString() + if (SystemUtils.IS_OS_WINDOWS && uriString.startsWith(FILE_UNC_PREFIX)) { + uriString = uriString.replace(FILE_UNC_PREFIX, "file://") + } + return uriString + } + + private fun createAuthority(rawAuthority: String?) = rawAuthority ?: "" + + private fun stripTrailingSlash(path: String?): String { + if (path.isNullOrEmpty()) return "" + return if (path.endsWith("/")) path.dropLast(1) else path + } + + private fun createPath(pathValue: String?) = ensurePrefix("/", pathValue) + + private fun createQuery(queryValue: String?) = ensurePrefix("?", queryValue) + + private fun createFragment(fragmentValue: String?) = ensurePrefix("#", fragmentValue) + + private fun ensurePrefix(prefix: String, value: String?): String { + if (value.isNullOrEmpty()) return "" + return if (value.startsWith(prefix)) value else prefix + value + } +} 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 deleted file mode 100644 index 0ec444a7..00000000 --- a/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.opencds.cqf.cql.ls.core.utility; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import org.junit.jupiter.api.Test; - -public class ConvertersTest { - - @Test - void should_returnInstance_when_creatingConverter() { - var converter = new Converters(); - assertNotNull(converter); - } - - @Test - void should_returnString_when_inputStreamExists() { - var expected = "The quick brown fox jumps over the lazy dog"; - try { - var actual = - Converters.inputStreamToString(new ByteArrayInputStream(expected.getBytes(StandardCharsets.UTF_8))); - assertEquals(expected, actual); - } catch (IOException e) { - fail("Unexpected exception thrown. {}", e); - } - } - - @Test - void should_returnStringWithLineBreaks_when_inputStreamHasLineBreaksExists() { - var expected = - """ - the first day in spring – - a wind from the ocean - but no ocean in sight"""; - try { - var actual = - Converters.inputStreamToString(new ByteArrayInputStream(expected.getBytes(StandardCharsets.UTF_8))); - assertEquals(expected, actual); - } catch (IOException e) { - fail("Unexpected exception thrown. {}", e); - } - } - - @Test - void should_throwIOException_when_inputStreamToStringHasAnIOError() throws IOException { - InputStream inputStream = mock(InputStream.class); - when(inputStream.read()).thenThrow(new IOException("Simulated failure")); - assertThrows(IOException.class, () -> Converters.inputStreamToString(inputStream)); - } - - @Test - void should_returnSource_when_stringExists() { - var expected = Converters.stringToSource("The quick brown fox jumps over the lazy dog"); - assertNotNull(expected); - } - - @Test - void should_returnSource_when_inputStreamExists() { - try { - var expected = Converters.inputStreamToSource(new ByteArrayInputStream( - "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8))); - assertNotNull(expected); - } catch (IOException e) { - fail("Unexpected exception thrown. {}", e); - } - } - - @Test - void should_throwIOException_when_inputStreamToSourceCalledWithNull() throws IOException { - InputStream inputStream = mock(InputStream.class); - when(inputStream.read()).thenThrow(new IOException("Simulated failure")); - assertThrows(IOException.class, () -> Converters.inputStreamToSource(inputStream)); - } -} diff --git a/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.kt b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.kt new file mode 100644 index 00000000..7e08af82 --- /dev/null +++ b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/ConvertersTest.kt @@ -0,0 +1,63 @@ +package org.opencds.cqf.cql.ls.core.utility + +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.nio.charset.StandardCharsets + +class ConvertersTest { + + @Test + fun should_returnInstance_when_creatingConverter() { + // Converters is a Kotlin object (singleton); verify it is accessible + assertNotNull(Converters) + } + + @Test + fun should_returnString_when_inputStreamExists() { + val expected = "The quick brown fox jumps over the lazy dog" + val actual = Converters.inputStreamToString(ByteArrayInputStream(expected.toByteArray(StandardCharsets.UTF_8))) + assertEquals(expected, actual) + } + + @Test + fun should_returnStringWithLineBreaks_when_inputStreamHasLineBreaksExists() { + val expected = "the first day in spring –\na wind from the ocean\nbut no ocean in sight" + val actual = Converters.inputStreamToString(ByteArrayInputStream(expected.toByteArray(StandardCharsets.UTF_8))) + assertEquals(expected, actual) + } + + @Test + fun should_throwIOException_when_inputStreamToStringHasAnIOError() { + val inputStream = mock(InputStream::class.java) + `when`(inputStream.read()).thenThrow(IOException("Simulated failure")) + assertThrows(IOException::class.java) { Converters.inputStreamToString(inputStream) } + } + + @Test + fun should_returnSource_when_stringExists() { + val expected = Converters.stringToSource("The quick brown fox jumps over the lazy dog") + assertNotNull(expected) + } + + @Test + fun should_returnSource_when_inputStreamExists() { + val expected = Converters.inputStreamToSource( + ByteArrayInputStream("The quick brown fox jumps over the lazy dog".toByteArray(StandardCharsets.UTF_8)) + ) + assertNotNull(expected) + } + + @Test + fun should_throwIOException_when_inputStreamToSourceCalledWithNull() { + val inputStream = mock(InputStream::class.java) + `when`(inputStream.read()).thenThrow(IOException("Simulated failure")) + assertThrows(IOException::class.java) { Converters.inputStreamToSource(inputStream) } + } +} diff --git a/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.java b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.java deleted file mode 100644 index c347230e..00000000 --- a/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.opencds.cqf.cql.ls.core.utility; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.net.URI; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnOs; -import org.junit.jupiter.api.condition.OS; - -class UrisTest { - - @Test - @EnabledOnOs({OS.MAC, OS.LINUX}) - void encodedUriGetHead() { - URI uri = Uris.parseOrNull("file:///d%3A/src/test.cql"); - URI root = Uris.getHead(uri); - assertEquals("file:///d%3A/src", root.toString()); - assertEquals("test.cql", root.relativize(uri).toString()); - } - - @Test - @EnabledOnOs({OS.WINDOWS}) - void encodedUriGetHeadWindows() { - URI uri = Uris.parseOrNull("file:///d%3A/src/test.cql"); - URI root = Uris.getHead(uri); - assertEquals("file:///d:/src", root.toString()); - assertEquals("test.cql", root.relativize(uri).toString()); - } - - @Test - void unencodedUriGetHeadHttps() { - URI uri = Uris.parseOrNull("https://hl7.org/fhir/test.cql"); - URI root = Uris.getHead(uri); - assertEquals("https://hl7.org/fhir", root.toString()); - assertEquals("test.cql", root.relativize(uri).toString()); - } - - @Test - @EnabledOnOs({OS.MAC, OS.LINUX}) - void unencodedUriGetHeadFile() { - URI uri = Uris.parseOrNull("file:///home/src/test.cql"); - URI root = Uris.getHead(uri); - assertEquals("file:///home/src", root.toString()); - assertEquals("test.cql", root.relativize(uri).toString()); - } - - @Test - @EnabledOnOs(OS.WINDOWS) - void unencodedUriGetHeadFileWindows() { - URI uri = Uris.parseOrNull("file:///home/src/test.cql"); - URI root = Uris.getHead(uri); - assertEquals("file:////home/src", root.toString()); - assertEquals("test.cql", root.relativize(uri).toString()); - } - - @Test - void uriAddPathHttp() { - URI root = Uris.parseOrNull("http://localhost:8080/home/src"); - URI uri = Uris.addPath(root, "test.cql"); - assertEquals("http://localhost:8080/home/src/test.cql", uri.toString()); - } - - @Test - @EnabledOnOs({OS.MAC, OS.LINUX}) - void uriAddPath() { - // With trailing slash - URI root = Uris.parseOrNull("file:///d%3A/src/"); - URI uri = Uris.addPath(root, "test.cql"); - assertEquals("file:///d%3A/src/test.cql", uri.toString()); - - uri = Uris.addPath(root, "/test.cql"); - assertEquals("file:///d%3A/src/test.cql", uri.toString()); - - // Unencoded - root = Uris.parseOrNull("file:///home/src"); - uri = Uris.addPath(root, "test.cql"); - assertEquals("file:///home/src/test.cql", uri.toString()); - } - - @Test - @EnabledOnOs(OS.WINDOWS) - void uriAddPathWindows() { - // With trailing slash - URI root = Uris.parseOrNull("file:///d%3A/src/"); - URI uri = Uris.addPath(root, "test.cql"); - assertEquals("file:///d:/src/test.cql", uri.toString()); - - uri = Uris.addPath(root, "/test.cql"); - assertEquals("file:///d:/src/test.cql", uri.toString()); - - // UNC path - root = Uris.parseOrNull("file:///home/src"); - uri = Uris.addPath(root, "test.cql"); - assertEquals("file:////home/src/test.cql", uri.toString()); - } - - @Test - void uriWithPath() { - URI initial = Uris.parseOrNull("file:///d%3A/src/"); - URI uri = Uris.withPath(initial, "/home/src/test.cql"); - assertEquals("file:///home/src/test.cql", uri.toString()); - - uri = Uris.withPath(initial, "home/src/test.cql"); - assertEquals("file:///home/src/test.cql", uri.toString()); - - // With authority - initial = Uris.parseOrNull("https://hl7.org/fhir"); - uri = Uris.withPath(initial, "/fhir/test.cql"); - assertEquals("https://hl7.org/fhir/test.cql", uri.toString()); - - uri = Uris.withPath(initial, "fhir/test.cql"); - assertEquals("https://hl7.org/fhir/test.cql", uri.toString()); - - // With trailing slash on URI - initial = Uris.parseOrNull("http://locahost:8080/fhir/"); - uri = Uris.withPath(initial, "/fhir/test.cql"); - assertEquals("http://locahost:8080/fhir/test.cql", uri.toString()); - - uri = Uris.withPath(initial, "fhir/test.cql"); - assertEquals("http://locahost:8080/fhir/test.cql", uri.toString()); - } - - @Test - @EnabledOnOs({OS.MAC, OS.LINUX}) - void toClientStringUnix() { - URI initial = Uris.parseOrNull("file:////d%3A/src/"); - String client = Uris.toClientUri(initial); - assertEquals("file:////d%3A/src/", client); - } - - @Test - @EnabledOnOs({OS.WINDOWS}) - void toClientStringWindows() { - URI initial = Uris.parseOrNull("file://d%3A/src/"); - String client = Uris.toClientUri(initial); - assertEquals("file:/d:/src", client); - - initial = Uris.parseOrNull("file:////d%3A/src/"); - client = Uris.toClientUri(initial); - assertEquals("file:/d:/src", client); - - initial = Uris.parseOrNull("file:/d%3A/src/"); - client = Uris.toClientUri(initial); - assertEquals("file:/d:/src", client); - } -} diff --git a/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt new file mode 100644 index 00000000..b08981f4 --- /dev/null +++ b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt @@ -0,0 +1,138 @@ +package org.opencds.cqf.cql.ls.core.utility + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledOnOs +import org.junit.jupiter.api.condition.OS + +class UrisTest { + + @Test + @EnabledOnOs(OS.MAC, OS.LINUX) + fun encodedUriGetHead() { + val uri = Uris.parseOrNull("file:///d%3A/src/test.cql")!! + val root = Uris.getHead(uri) + assertEquals("file:///d%3A/src", root.toString()) + assertEquals("test.cql", root.relativize(uri).toString()) + } + + @Test + @EnabledOnOs(OS.WINDOWS) + fun encodedUriGetHeadWindows() { + val uri = Uris.parseOrNull("file:///d%3A/src/test.cql")!! + val root = Uris.getHead(uri) + assertEquals("file:///d:/src", root.toString()) + assertEquals("test.cql", root.relativize(uri).toString()) + } + + @Test + fun unencodedUriGetHeadHttps() { + val uri = Uris.parseOrNull("https://hl7.org/fhir/test.cql")!! + val root = Uris.getHead(uri) + assertEquals("https://hl7.org/fhir", root.toString()) + assertEquals("test.cql", root.relativize(uri).toString()) + } + + @Test + @EnabledOnOs(OS.MAC, OS.LINUX) + fun unencodedUriGetHeadFile() { + val uri = Uris.parseOrNull("file:///home/src/test.cql")!! + val root = Uris.getHead(uri) + assertEquals("file:///home/src", root.toString()) + assertEquals("test.cql", root.relativize(uri).toString()) + } + + @Test + @EnabledOnOs(OS.WINDOWS) + fun unencodedUriGetHeadFileWindows() { + val uri = Uris.parseOrNull("file:///home/src/test.cql")!! + val root = Uris.getHead(uri) + assertEquals("file:////home/src", root.toString()) + assertEquals("test.cql", root.relativize(uri).toString()) + } + + @Test + fun uriAddPathHttp() { + val root = Uris.parseOrNull("http://localhost:8080/home/src")!! + val uri = Uris.addPath(root, "test.cql") + assertEquals("http://localhost:8080/home/src/test.cql", uri.toString()) + } + + @Test + @EnabledOnOs(OS.MAC, OS.LINUX) + fun uriAddPath() { + var root = Uris.parseOrNull("file:///d%3A/src/")!! + var uri = Uris.addPath(root, "test.cql") + assertEquals("file:///d%3A/src/test.cql", uri.toString()) + + uri = Uris.addPath(root, "/test.cql") + assertEquals("file:///d%3A/src/test.cql", uri.toString()) + + root = Uris.parseOrNull("file:///home/src")!! + uri = Uris.addPath(root, "test.cql") + assertEquals("file:///home/src/test.cql", uri.toString()) + } + + @Test + @EnabledOnOs(OS.WINDOWS) + fun uriAddPathWindows() { + var root = Uris.parseOrNull("file:///d%3A/src/")!! + var uri = Uris.addPath(root, "test.cql") + assertEquals("file:///d:/src/test.cql", uri.toString()) + + uri = Uris.addPath(root, "/test.cql") + assertEquals("file:///d:/src/test.cql", uri.toString()) + + root = Uris.parseOrNull("file:///home/src")!! + uri = Uris.addPath(root, "test.cql") + assertEquals("file:////home/src/test.cql", uri.toString()) + } + + @Test + fun uriWithPath() { + var initial = Uris.parseOrNull("file:///d%3A/src/")!! + var uri = Uris.withPath(initial, "/home/src/test.cql") + assertEquals("file:///home/src/test.cql", uri.toString()) + + uri = Uris.withPath(initial, "home/src/test.cql") + assertEquals("file:///home/src/test.cql", uri.toString()) + + initial = Uris.parseOrNull("https://hl7.org/fhir")!! + uri = Uris.withPath(initial, "/fhir/test.cql") + assertEquals("https://hl7.org/fhir/test.cql", uri.toString()) + + uri = Uris.withPath(initial, "fhir/test.cql") + assertEquals("https://hl7.org/fhir/test.cql", uri.toString()) + + initial = Uris.parseOrNull("http://locahost:8080/fhir/")!! + uri = Uris.withPath(initial, "/fhir/test.cql") + assertEquals("http://locahost:8080/fhir/test.cql", uri.toString()) + + uri = Uris.withPath(initial, "fhir/test.cql") + assertEquals("http://locahost:8080/fhir/test.cql", uri.toString()) + } + + @Test + @EnabledOnOs(OS.MAC, OS.LINUX) + fun toClientStringUnix() { + val initial = Uris.parseOrNull("file:////d%3A/src/")!! + val client = Uris.toClientUri(initial) + assertEquals("file:////d%3A/src/", client) + } + + @Test + @EnabledOnOs(OS.WINDOWS) + fun toClientStringWindows() { + var initial = Uris.parseOrNull("file://d%3A/src/")!! + var client = Uris.toClientUri(initial) + assertEquals("file:/d:/src", client) + + initial = Uris.parseOrNull("file:////d%3A/src/")!! + client = Uris.toClientUri(initial) + assertEquals("file:/d:/src", client) + + initial = Uris.parseOrNull("file:/d%3A/src/")!! + client = Uris.toClientUri(initial) + assertEquals("file:/d:/src", client) + } +} diff --git a/debug/server/src/main/java/org/opencds/cqf/cql/debug/CqlDebugServer.java b/debug/server/src/main/java/org/opencds/cqf/cql/debug/CqlDebugServer.java deleted file mode 100644 index 1aec38a9..00000000 --- a/debug/server/src/main/java/org/opencds/cqf/cql/debug/CqlDebugServer.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.opencds.cqf.cql.debug; - -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.debug.Capabilities; -import org.eclipse.lsp4j.debug.ConfigurationDoneArguments; -import org.eclipse.lsp4j.debug.DisconnectArguments; -import org.eclipse.lsp4j.debug.ExitedEventArguments; -import org.eclipse.lsp4j.debug.InitializeRequestArguments; -import org.eclipse.lsp4j.debug.TerminatedEventArguments; -import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; -import org.eclipse.lsp4j.debug.services.IDebugProtocolServer; - -public class CqlDebugServer implements IDebugProtocolServer, IDebugProtocolClientAware { - private CompletableFuture exited; - private ServerState serverState; - - public CqlDebugServer() { - this.exited = new CompletableFuture<>(); - this.serverState = ServerState.STARTED; - } - - protected final CompletableFuture client = new CompletableFuture<>(); - - @Override - public void connect(IDebugProtocolClient client) { - this.client.complete(client); - } - - @Override - public CompletableFuture initialize(InitializeRequestArguments args) { - checkState(ServerState.STARTED); - // TODO: Work through all the capabilities we should support. - Capabilities capabilities = new Capabilities(); - capabilities.setSupportsConfigurationDoneRequest(true); - /// And... nothing else at this point. - - setState(ServerState.INITIALIZED); - return CompletableFuture.completedFuture(capabilities) - .whenCompleteAsync((c, e) -> this.client.join().initialized()); - } - - @Override - public CompletableFuture configurationDone(ConfigurationDoneArguments args) { - checkState(ServerState.INITIALIZED); - setState(ServerState.CONFIGURED); - return CompletableFuture.completedFuture(null); - } - - @Override - public CompletableFuture disconnect(DisconnectArguments args) { - return CompletableFuture.runAsync(() -> {}).whenCompleteAsync((o, e) -> this.exitServer()); - } - - @Override - public CompletableFuture launch(Map args) { - checkState(ServerState.CONFIGURED); - setState(ServerState.RUNNING); - - // Create CQL request arguments.. - return CompletableFuture.runAsync(() -> {}).thenRunAsync(() -> { - this.terminateServer(); - this.exitServer(); - }); - } - - protected void terminateServer() { - this.terminateServer(null); - } - - protected void terminateServer(Object restart) { - TerminatedEventArguments terminatedEventArguments = new TerminatedEventArguments(); - terminatedEventArguments.setRestart(restart); - this.client.join().terminated(terminatedEventArguments); - } - - protected void exitServer() { - this.exitServer(0); - } - - protected void exitServer(int exitCode) { - ExitedEventArguments exitedEventArguments = new ExitedEventArguments(); - exitedEventArguments.setExitCode(exitCode); - this.client.join().exited(exitedEventArguments); - setState(ServerState.STOPPED); - this.exited.complete(null); - } - - public CompletableFuture exited() { - return this.exited; - } - - protected void checkState(ServerState requiredState) { - if (this.serverState != requiredState) { - throw new IllegalStateException(String.format( - "Operation required state %s, server actual state: %s", - requiredState.toString(), this.serverState.toString())); - } - } - - protected void setState(ServerState newState) { - this.serverState = newState; - } - - public ServerState getState() { - return this.serverState; - } -} diff --git a/debug/server/src/main/java/org/opencds/cqf/cql/debug/CqlDebugServer.kt b/debug/server/src/main/java/org/opencds/cqf/cql/debug/CqlDebugServer.kt new file mode 100644 index 00000000..7aeb2ced --- /dev/null +++ b/debug/server/src/main/java/org/opencds/cqf/cql/debug/CqlDebugServer.kt @@ -0,0 +1,97 @@ +package org.opencds.cqf.cql.debug + +import org.eclipse.lsp4j.debug.Capabilities +import org.eclipse.lsp4j.debug.ConfigurationDoneArguments +import org.eclipse.lsp4j.debug.DisconnectArguments +import org.eclipse.lsp4j.debug.ExitedEventArguments +import org.eclipse.lsp4j.debug.InitializeRequestArguments +import org.eclipse.lsp4j.debug.TerminatedEventArguments +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer +import java.util.concurrent.CompletableFuture + +open class CqlDebugServer : IDebugProtocolServer, IDebugProtocolClientAware { + private var exited: CompletableFuture = CompletableFuture() + private var serverState: ServerState = ServerState.STARTED + + protected val client: CompletableFuture = CompletableFuture() + + override fun connect(client: IDebugProtocolClient) { + this.client.complete(client) + } + + override fun initialize(args: InitializeRequestArguments): CompletableFuture { + checkState(ServerState.STARTED) + // TODO: Work through all the capabilities we should support. + val capabilities = Capabilities() + capabilities.supportsConfigurationDoneRequest = true + /// And... nothing else at this point. + + setState(ServerState.INITIALIZED) + return CompletableFuture.completedFuture(capabilities) + .whenCompleteAsync { _, _ -> this.client.join().initialized() } + } + + override fun configurationDone(args: ConfigurationDoneArguments): CompletableFuture { + checkState(ServerState.INITIALIZED) + setState(ServerState.CONFIGURED) + return CompletableFuture.completedFuture(null) + } + + override fun disconnect(args: DisconnectArguments): CompletableFuture { + return CompletableFuture.runAsync {}.whenCompleteAsync { _, _ -> this.exitServer() } + } + + override fun launch(args: Map): CompletableFuture { + checkState(ServerState.CONFIGURED) + setState(ServerState.RUNNING) + + // Create CQL request arguments.. + return CompletableFuture.runAsync {}.thenRunAsync { + this.terminateServer() + this.exitServer() + } + } + + protected fun terminateServer() { + this.terminateServer(null) + } + + protected fun terminateServer(restart: Any?) { + val terminatedEventArguments = TerminatedEventArguments() + terminatedEventArguments.restart = restart + this.client.join().terminated(terminatedEventArguments) + } + + protected fun exitServer() { + this.exitServer(0) + } + + protected fun exitServer(exitCode: Int) { + val exitedEventArguments = ExitedEventArguments() + exitedEventArguments.exitCode = exitCode + this.client.join().exited(exitedEventArguments) + setState(ServerState.STOPPED) + this.exited.complete(null) + } + + fun exited(): CompletableFuture { + return this.exited + } + + protected fun checkState(requiredState: ServerState) { + if (this.serverState != requiredState) { + throw IllegalStateException( + "Operation required state ${requiredState}, server actual state: ${this.serverState}" + ) + } + } + + protected fun setState(newState: ServerState) { + this.serverState = newState + } + + fun getState(): ServerState { + return this.serverState + } +} diff --git a/debug/server/src/main/java/org/opencds/cqf/cql/debug/IDebugProtocolClientAware.java b/debug/server/src/main/java/org/opencds/cqf/cql/debug/IDebugProtocolClientAware.java deleted file mode 100644 index 16f5f9e4..00000000 --- a/debug/server/src/main/java/org/opencds/cqf/cql/debug/IDebugProtocolClientAware.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.opencds.cqf.cql.debug; - -import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; - -public interface IDebugProtocolClientAware { - void connect(IDebugProtocolClient client); -} diff --git a/debug/server/src/main/java/org/opencds/cqf/cql/debug/IDebugProtocolClientAware.kt b/debug/server/src/main/java/org/opencds/cqf/cql/debug/IDebugProtocolClientAware.kt new file mode 100644 index 00000000..c42319bb --- /dev/null +++ b/debug/server/src/main/java/org/opencds/cqf/cql/debug/IDebugProtocolClientAware.kt @@ -0,0 +1,7 @@ +package org.opencds.cqf.cql.debug + +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient + +interface IDebugProtocolClientAware { + fun connect(client: IDebugProtocolClient) +} diff --git a/debug/server/src/main/java/org/opencds/cqf/cql/debug/ServerState.java b/debug/server/src/main/java/org/opencds/cqf/cql/debug/ServerState.kt similarity index 54% rename from debug/server/src/main/java/org/opencds/cqf/cql/debug/ServerState.java rename to debug/server/src/main/java/org/opencds/cqf/cql/debug/ServerState.kt index 169cfa9a..5c49f7f6 100644 --- a/debug/server/src/main/java/org/opencds/cqf/cql/debug/ServerState.java +++ b/debug/server/src/main/java/org/opencds/cqf/cql/debug/ServerState.kt @@ -1,6 +1,6 @@ -package org.opencds.cqf.cql.debug; +package org.opencds.cqf.cql.debug -public enum ServerState { +enum class ServerState { STARTED, INITIALIZED, CONFIGURED, diff --git a/debug/server/src/test/java/org/opencds/cqf/cql/debug/DebugServerTest.java b/debug/server/src/test/java/org/opencds/cqf/cql/debug/DebugServerTest.java deleted file mode 100644 index e5081b56..00000000 --- a/debug/server/src/test/java/org/opencds/cqf/cql/debug/DebugServerTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.opencds.cqf.cql.debug; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; - -import java.util.HashMap; -import org.eclipse.lsp4j.debug.Capabilities; -import org.eclipse.lsp4j.debug.ConfigurationDoneArguments; -import org.eclipse.lsp4j.debug.InitializeRequestArguments; -import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -class DebugServerTest { - - @Test - void handshake() throws Exception { - IDebugProtocolClient client = Mockito.mock(IDebugProtocolClient.class); - CqlDebugServer server = new CqlDebugServer(); - server.connect(client); - - assertEquals(ServerState.STARTED, server.getState()); - - // https://microsoft.github.io/debug-adapter-protocol/overview - - // Sequence for initialization - // initialize - // (initialized) - // setBreakpoints - // setFunctionBreakpoints - // setExceptionBreakpoints - - // do debugging loop... - - // server terminated - // server exited - Capabilities capabilities = - server.initialize(new InitializeRequestArguments()).get(); - assertNotNull(capabilities); - - // Server should send the "initialized" event once it's ready - Mockito.verify(client).initialized(); - assertEquals(ServerState.INITIALIZED, server.getState()); - - // SetBreakpointsResponse setBreakpointsResponse = server.setBreakpoints(new - // SetBreakpointsArguments()).get(); - // assertNotNull(setBreakpointsResponse); - - // SetFunctionBreakpointsResponse setFunctionBreakpointsResponse = - // server.setFunctionBreakpoints(new SetFunctionBreakpointsArguments()).get(); - // assertNotNull(setFunctionBreakpointsResponse); - - // TODO: in DAP 1.47+ this has a return type. - // server.setExceptionBreakpoints(new SetExceptionBreakpointsArguments()).get(); - - server.configurationDone(new ConfigurationDoneArguments()).get(); - assertEquals(ServerState.CONFIGURED, server.getState()); - - // Server should now be ready to launch... - // The "launch" options are specific to the CQL implementation - // Essentially, key-value pairs. - server.launch(new HashMap<>()).get(); - - // Breakpoints hit and so on... - - // terminated, and then exited - Mockito.verify(client).terminated(any()); - Mockito.verify(client).exited(any()); - assertEquals(ServerState.STOPPED, server.getState()); - } -} diff --git a/debug/server/src/test/java/org/opencds/cqf/cql/debug/DebugServerTest.kt b/debug/server/src/test/java/org/opencds/cqf/cql/debug/DebugServerTest.kt new file mode 100644 index 00000000..f9ee0589 --- /dev/null +++ b/debug/server/src/test/java/org/opencds/cqf/cql/debug/DebugServerTest.kt @@ -0,0 +1,69 @@ +package org.opencds.cqf.cql.debug + +import org.eclipse.lsp4j.debug.Capabilities +import org.eclipse.lsp4j.debug.ConfigurationDoneArguments +import org.eclipse.lsp4j.debug.InitializeRequestArguments +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito + +class DebugServerTest { + + @Test + fun handshake() { + val client = Mockito.mock(IDebugProtocolClient::class.java) + val server = CqlDebugServer() + server.connect(client) + + assertEquals(ServerState.STARTED, server.getState()) + + // https://microsoft.github.io/debug-adapter-protocol/overview + + // Sequence for initialization + // initialize + // (initialized) + // setBreakpoints + // setFunctionBreakpoints + // setExceptionBreakpoints + + // do debugging loop... + + // server terminated + // server exited + val capabilities: Capabilities = server.initialize(InitializeRequestArguments()).get() + assertNotNull(capabilities) + + // Server should send the "initialized" event once it's ready + Mockito.verify(client).initialized() + assertEquals(ServerState.INITIALIZED, server.getState()) + + // SetBreakpointsResponse setBreakpointsResponse = server.setBreakpoints(new + // SetBreakpointsArguments()).get(); + // assertNotNull(setBreakpointsResponse); + + // SetFunctionBreakpointsResponse setFunctionBreakpointsResponse = + // server.setFunctionBreakpoints(new SetFunctionBreakpointsArguments()).get(); + // assertNotNull(setFunctionBreakpointsResponse); + + // TODO: in DAP 1.47+ this has a return type. + // server.setExceptionBreakpoints(new SetExceptionBreakpointsArguments()).get(); + + server.configurationDone(ConfigurationDoneArguments()).get() + assertEquals(ServerState.CONFIGURED, server.getState()) + + // Server should now be ready to launch... + // The "launch" options are specific to the CQL implementation + // Essentially, key-value pairs. + server.launch(HashMap()).get() + + // Breakpoints hit and so on... + + // terminated, and then exited + Mockito.verify(client).terminated(any()) + Mockito.verify(client).exited(any()) + assertEquals(ServerState.STOPPED, server.getState()) + } +} diff --git a/debug/service/pom.xml b/debug/service/pom.xml index bbbd2bf4..00e5651e 100644 --- a/debug/service/pom.xml +++ b/debug/service/pom.xml @@ -23,36 +23,41 @@ ${project.version} - org.springframework.boot - spring-boot + ch.qos.logback + logback-classic - org.springframework.boot - spring-boot-starter-logging + org.slf4j + jul-to-slf4j org.apache.maven.plugins - maven-jar-plugin - - - - true - org.opencds.cqf.cql.debug.service.Main - - - - - - org.springframework.boot - spring-boot-maven-plugin + maven-shade-plugin - - repackage - + package + shade + + + + org.opencds.cqf.cql.debug.service.MainKt + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + diff --git a/debug/service/src/main/java/org/opencds/cqf/cql/debug/service/Main.java b/debug/service/src/main/java/org/opencds/cqf/cql/debug/service/Main.java deleted file mode 100644 index 58ca7f7d..00000000 --- a/debug/service/src/main/java/org/opencds/cqf/cql/debug/service/Main.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.opencds.cqf.cql.debug.service; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import org.eclipse.lsp4j.debug.launch.DSPLauncher; -import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; -import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.opencds.cqf.cql.debug.CqlDebugServer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.bridge.SLF4JBridgeHandler; - -/** - * This class starts a CqlDebugServer listening on std-in and std-out. NOTE: This stand-alone server - * is not the primary way the CqlDebugServer is expected to be used. Rather, the typically usage - * will be as a plugin to the CqlLanguageServer to add debug capabilities - */ -public class Main { - - private static final Logger log = LoggerFactory.getLogger(Main.class); - - /** - * Entrypoint for the cql-debug-service - * - * @param args the commandline parameters (none supported currently) - * @throws InterruptedException if server thread is cancelled - * @throws ExecutionException if server thread errors - */ - public static void main(String[] args) throws InterruptedException, ExecutionException { - SLF4JBridgeHandler.removeHandlersForRootLogger(); - SLF4JBridgeHandler.install(); - - log.info("java.version is {}", System.getProperty("java.version")); - log.info("cql-debug version is {}", CqlDebugServer.class.getPackage().getImplementationVersion()); - - CqlDebugServer server = new CqlDebugServer(); - - @SuppressWarnings("java:S106") - Launcher launcher = DSPLauncher.createServerLauncher(server, System.in, System.out); - - server.connect(launcher.getRemoteProxy()); - Future serverThread = launcher.startListening(); - - server.exited().get(); - serverThread.cancel(true); - } -} diff --git a/debug/service/src/main/java/org/opencds/cqf/cql/debug/service/main.kt b/debug/service/src/main/java/org/opencds/cqf/cql/debug/service/main.kt new file mode 100644 index 00000000..eac40637 --- /dev/null +++ b/debug/service/src/main/java/org/opencds/cqf/cql/debug/service/main.kt @@ -0,0 +1,28 @@ +package org.opencds.cqf.cql.debug.service + +import org.eclipse.lsp4j.debug.launch.DSPLauncher +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient +import org.opencds.cqf.cql.debug.CqlDebugServer +import org.slf4j.LoggerFactory +import org.slf4j.bridge.SLF4JBridgeHandler + +private val log = LoggerFactory.getLogger("org.opencds.cqf.cql.debug.service.Main") + +fun main(args: Array) { + SLF4JBridgeHandler.removeHandlersForRootLogger() + SLF4JBridgeHandler.install() + + log.info("java.version is {}", System.getProperty("java.version")) + log.info("cql-debug version is {}", CqlDebugServer::class.java.`package`.implementationVersion) + + val server = CqlDebugServer() + + @Suppress("java:S106") + val launcher = DSPLauncher.createServerLauncher(server, System.`in`, System.out) + + server.connect(launcher.remoteProxy) + val serverThread = launcher.startListening() + + server.exited().get() + serverThread.cancel(true) +} diff --git a/ls/server/pom.xml b/ls/server/pom.xml index 862aca6f..efac1f69 100644 --- a/ls/server/pom.xml +++ b/ls/server/pom.xml @@ -23,11 +23,6 @@ 8.6.0 - - org.springframework.boot - spring-boot - - org.opencds.cqf.cql.ls cql-ls-core @@ -118,6 +113,33 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + + + kapt + kapt + + + ${project.basedir}/src/main/java + + + + com.google.auto.service + auto-service + ${auto-service.version} + + + info.picocli + picocli-codegen + ${picocli.version} + + + + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/Constants.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/Constants.java deleted file mode 100644 index f0438036..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/Constants.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.opencds.cqf.cql.ls.server; - -public class Constants { - - private Constants() {} - - // Randomly generated UUID for the watchedFiles registration (only watch options and ig.ini) - public static final String WORKSPACE_DID_CHANGE_WATCHED_FILES_ID = "f440e9c3-ce43-4080-a845-c17f7375fddd"; - public static final String WORKSPACE_DID_CHANGE_WATCHED_FILES_METHOD = "workspace/didChangeWatchedFiles"; -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/Constants.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/Constants.kt new file mode 100644 index 00000000..00b28870 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/Constants.kt @@ -0,0 +1,7 @@ +package org.opencds.cqf.cql.ls.server + +object Constants { + // Randomly generated UUID for the watchedFiles registration (only watch options and ig.ini) + const val WORKSPACE_DID_CHANGE_WATCHED_FILES_ID = "f440e9c3-ce43-4080-a845-c17f7375fddd" + const val WORKSPACE_DID_CHANGE_WATCHED_FILES_METHOD = "workspace/didChangeWatchedFiles" +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.java deleted file mode 100644 index e45a5f1a..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.opencds.cqf.cql.ls.server; - -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.InitializeParams; -import org.eclipse.lsp4j.InitializeResult; -import org.eclipse.lsp4j.InitializedParams; -import org.eclipse.lsp4j.ServerCapabilities; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.LanguageClientAware; -import org.eclipse.lsp4j.services.LanguageServer; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.eclipse.lsp4j.services.WorkspaceService; -import org.opencds.cqf.cql.ls.server.service.CqlTextDocumentService; -import org.opencds.cqf.cql.ls.server.service.CqlWorkspaceService; -import org.opencds.cqf.cql.ls.server.utility.Futures; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CqlLanguageServer implements LanguageServer, LanguageClientAware { - private static final Logger log = LoggerFactory.getLogger(CqlLanguageServer.class); - - private final CqlWorkspaceService workspaceService; - private final CqlTextDocumentService textDocumentService; - private final CompletableFuture client; - - private final CompletableFuture exited; - - public CqlLanguageServer( - CompletableFuture client, - CqlWorkspaceService workspaceService, - CqlTextDocumentService textDocumentService) { - this.exited = new CompletableFuture<>(); - this.client = client; - this.workspaceService = workspaceService; - this.textDocumentService = textDocumentService; - } - - @Override - public CompletableFuture initialize(InitializeParams params) { - try { - ServerCapabilities serverCapabilities = new ServerCapabilities(); - this.workspaceService.initialize(params, serverCapabilities); - this.textDocumentService.initialize(params, serverCapabilities); - - InitializeResult result = new InitializeResult(); - result.setCapabilities(serverCapabilities); - return CompletableFuture.completedFuture(result); - } catch (Exception e) { - log.error("failed to initialize with error: {}", e.getMessage()); - return Futures.failed(e); - } - } - - @Override - public void initialized(InitializedParams params) { - // Nothing to do, currently. - } - - @Override - public CompletableFuture shutdown() { - // Nothing to do currently - return CompletableFuture.completedFuture(null); - } - - @Override - public void exit() { - this.exited.complete(null); - } - - public CompletableFuture exited() { - return this.exited; - } - - @Override - public TextDocumentService getTextDocumentService() { - return textDocumentService; - } - - @Override - public WorkspaceService getWorkspaceService() { - return workspaceService; - } - - @Override - public void connect(LanguageClient client) { - this.client.complete(client); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.kt new file mode 100644 index 00000000..15df98f3 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.kt @@ -0,0 +1,67 @@ +package org.opencds.cqf.cql.ls.server + +import org.eclipse.lsp4j.InitializeParams +import org.eclipse.lsp4j.InitializeResult +import org.eclipse.lsp4j.InitializedParams +import org.eclipse.lsp4j.ServerCapabilities +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.services.LanguageClientAware +import org.eclipse.lsp4j.services.LanguageServer +import org.eclipse.lsp4j.services.TextDocumentService +import org.eclipse.lsp4j.services.WorkspaceService +import org.opencds.cqf.cql.ls.server.service.CqlTextDocumentService +import org.opencds.cqf.cql.ls.server.service.CqlWorkspaceService +import org.opencds.cqf.cql.ls.server.utility.Futures +import org.slf4j.LoggerFactory +import java.util.concurrent.CompletableFuture + +class CqlLanguageServer( + private val client: CompletableFuture, + private val workspaceService: CqlWorkspaceService, + private val textDocumentService: CqlTextDocumentService +) : LanguageServer, LanguageClientAware { + + companion object { + private val log = LoggerFactory.getLogger(CqlLanguageServer::class.java) + } + + private val exited = CompletableFuture() + + override fun initialize(params: InitializeParams): CompletableFuture { + return try { + val serverCapabilities = ServerCapabilities() + workspaceService.initialize(params, serverCapabilities) + textDocumentService.initialize(params, serverCapabilities) + + val result = InitializeResult() + result.capabilities = serverCapabilities + CompletableFuture.completedFuture(result) + } catch (e: Exception) { + log.error("failed to initialize with error: {}", e.message) + Futures.failed(e) + } + } + + override fun initialized(params: InitializedParams) { + // Nothing to do, currently. + } + + override fun shutdown(): CompletableFuture { + // Nothing to do currently + return CompletableFuture.completedFuture(null) + } + + override fun exit() { + exited.complete(null) + } + + fun exited(): CompletableFuture = exited + + override fun getTextDocumentService(): TextDocumentService = textDocumentService + + override fun getWorkspaceService(): WorkspaceService = workspaceService + + override fun connect(client: LanguageClient) { + this.client.complete(client) + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CliCommand.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CliCommand.java deleted file mode 100644 index 3cbd0620..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CliCommand.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.opencds.cqf.cql.ls.server.command; - -import org.opencds.cqf.cql.ls.server.manager.IgContextManager; -import picocli.CommandLine.Command; - -@Command(subcommands = {CqlCommand.class}) -public class CliCommand { - private IgContextManager igContextManager; - - public IgContextManager getIgContextManager() { - return this.igContextManager; - } - - public CliCommand(IgContextManager igContextManager) { - this.igContextManager = igContextManager; - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CliCommand.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CliCommand.kt new file mode 100644 index 00000000..650d2318 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CliCommand.kt @@ -0,0 +1,9 @@ +package org.opencds.cqf.cql.ls.server.command + +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import picocli.CommandLine.Command + +@Command(subcommands = [CqlCommand::class]) +class CliCommand(private val igContextManager: IgContextManager) { + fun getIgContextManager(): IgContextManager = igContextManager +} 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 deleted file mode 100644 index d39eaa1e..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.java +++ /dev/null @@ -1,333 +0,0 @@ -package org.opencds.cqf.cql.ls.server.command; - -import static kotlinx.io.files.PathsKt.Path; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.repository.IRepository; -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; -import org.apache.commons.lang3.tuple.Pair; -import org.cqframework.cql.cql2elm.CqlTranslatorOptions; -import org.cqframework.cql.cql2elm.DefaultLibrarySourceProvider; -import org.cqframework.cql.cql2elm.DefaultModelInfoProvider; -import org.cqframework.fhir.npm.NpmProcessor; -import org.cqframework.fhir.utilities.IGContext; -import org.hl7.elm.r1.VersionedIdentifier; -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.ILoggingService; -import org.opencds.cqf.cql.engine.execution.EvaluationResult; -import org.opencds.cqf.cql.engine.execution.ExpressionResult; -import org.opencds.cqf.cql.ls.core.utility.Uris; -import org.opencds.cqf.cql.ls.server.repository.ig.standard.IgStandardRepository; -import org.opencds.cqf.fhir.cql.CqlOptions; -import org.opencds.cqf.fhir.cql.Engines; -import org.opencds.cqf.fhir.cql.EvaluationSettings; -import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings; -import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.PROFILE_MODE; -import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; -import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; -import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings; -import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.CODE_LOOKUP_MODE; -import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; -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.slf4j.LoggerFactory; -import picocli.CommandLine; -import picocli.CommandLine.ArgGroup; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; - -@Command(name = "cql", mixinStandardHelpOptions = true) -public class CqlCommand implements Callable { - @Option( - names = {"-fv", "--fhir-version"}, - required = true) - public String fhirVersion; - - @Option(names = {"-op", "--options-path"}) - public String optionsPath; - - @ArgGroup(multiplicity = "0..1", exclusive = false) - public NamespaceParameter namespace; - - static class NamespaceParameter { - @Option(names = {"-nn", "--namespace-name"}) - public String namespaceName; - - @Option(names = {"-nu", "--namespace-uri"}) - public String namespaceUri; - } - - @Option(names = {"-rd", "--root-dir"}) - public String rootDir; - - @Option(names = {"-ig", "--ig-path"}) - public String igPath; - - @ArgGroup(multiplicity = "1..*", exclusive = false) - List libraries; - - static class LibraryParameter { - @Option( - names = {"-lu", "--library-url"}, - required = true) - public String libraryUrl; - - @Option( - names = {"-ln", "--library-name"}, - required = true) - public String libraryName; - - @Option(names = {"-lv", "--library-version"}) - public String libraryVersion; - - @Option(names = {"-t", "--terminology-url"}) - public String terminologyUrl; - - @ArgGroup(multiplicity = "0..1", exclusive = false) - public ModelParameter model; - - @ArgGroup(multiplicity = "0..*", exclusive = false) - public List parameters; - - @Option(names = {"-e", "--expression"}) - public String[] expression; - - @ArgGroup(multiplicity = "0..1", exclusive = false) - public ContextParameter context; - - static class ContextParameter { - @Option(names = {"-c", "--context"}) - public String contextName; - - @Option(names = {"-cv", "--context-value"}) - public String contextValue; - } - - static class ModelParameter { - @Option(names = {"-m", "--model"}) - public String modelName; - - @Option(names = {"-mu", "--model-url"}) - public String modelUrl; - } - - static class ParameterParameter { - @Option(names = {"-p", "--parameter"}) - public String parameterName; - - @Option(names = {"-pv", "--parameter-value"}) - public String parameterValue; - } - } - - @SuppressWarnings("removal") // TODO: Missed a spot upstream in the CQL library - private static class Logger implements ILoggingService { - - private final org.slf4j.Logger log = LoggerFactory.getLogger(Logger.class); - - @Override - public void logMessage(String s) { - log.warn(s); - } - - @Override - public void logDebugMessage(LogCategory logCategory, String s) { - log.debug("{}: {}", logCategory, s); - } - - @Override - public boolean isDebugLogging() { - return log.isDebugEnabled(); - } - } - - private String toVersionNumber(FhirVersionEnum 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 - private CliCommand parentCommand; - - @Override - public Integer call() throws Exception { - - FhirVersionEnum fhirVersionEnum = FhirVersionEnum.valueOf(fhirVersion); - - FhirContext fhirContext = FhirContext.forCached(fhirVersionEnum); - - IGContext igContext = null; - NpmProcessor npmProcessor = null; - if (rootDir != null && igPath != null) { - igContext = new IGContext(new Logger()); - igContext.initializeFromIg(rootDir, igPath, toVersionNumber(fhirVersionEnum)); - } else if (parentCommand != null && parentCommand.getIgContextManager() != null && rootDir != null) { - npmProcessor = parentCommand - .getIgContextManager() - .getContext(Uris.addPath(Uris.addPath(URI.create(rootDir), "input"), "cql")); - if (npmProcessor != null) { - igContext = npmProcessor.getIgContext(); - } - } - - if (npmProcessor == null) { - npmProcessor = new NpmProcessor(igContext); - } - - CqlOptions cqlOptions = CqlOptions.defaultOptions(); - - if (optionsPath != null) { - var op = Path(Uris.parseOrNull(optionsPath).toURL().getPath()); - CqlTranslatorOptions options = CqlTranslatorOptions.fromFile(Path(op)); - cqlOptions.setCqlCompilerOptions(options.getCqlCompilerOptions()); - } - - var terminologySettings = new TerminologySettings(); - terminologySettings.setValuesetExpansionMode(VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION); - terminologySettings.setValuesetPreExpansionMode(VALUESET_PRE_EXPANSION_MODE.USE_IF_PRESENT); - terminologySettings.setValuesetMembershipMode(VALUESET_MEMBERSHIP_MODE.USE_EXPANSION); - terminologySettings.setCodeLookupMode(CODE_LOOKUP_MODE.USE_CODESYSTEM_URL); - - var retrieveSettings = new RetrieveSettings(); - retrieveSettings.setTerminologyParameterMode(TERMINOLOGY_FILTER_MODE.FILTER_IN_MEMORY); - retrieveSettings.setSearchParameterMode(SEARCH_FILTER_MODE.FILTER_IN_MEMORY); - retrieveSettings.setProfileMode(PROFILE_MODE.DECLARED); - - var evaluationSettings = EvaluationSettings.getDefault(); - evaluationSettings.setCqlOptions(cqlOptions); - evaluationSettings.setTerminologySettings(terminologySettings); - evaluationSettings.setRetrieveSettings(retrieveSettings); - evaluationSettings.setNpmProcessor(npmProcessor); - - for (LibraryParameter library : libraries) { - // 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 libraryUri = library.libraryUrl != null ? Uris.parseOrNull(library.libraryUrl) : null; - - var libraryKotlinPath = libraryUri != null ? Path(libraryUri.toURL().getPath()) : null; - - 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 engine = Engines.forRepository(repository, evaluationSettings); - - if (library.libraryUrl != null) { - var provider = new DefaultLibrarySourceProvider(libraryKotlinPath); - engine.getEnvironment() - .getLibraryManager() - .getLibrarySourceLoader() - .registerProvider(provider); - - var modelProvider = new DefaultModelInfoProvider(libraryKotlinPath); - engine.getEnvironment() - .getLibraryManager() - .getModelManager() - .getModelInfoLoader() - .registerModelInfoProvider(modelProvider); - } - - VersionedIdentifier identifier = new VersionedIdentifier().withId(library.libraryName); - - Pair contextParameter = null; - - if (library.context != null) { - contextParameter = Pair.of(library.context.contextName, library.context.contextValue); - } - - EvaluationResult result = engine.evaluate(identifier, contextParameter); - - writeResult(result); - } - - return 0; - } - - private IRepository createRepository(FhirContext fhirContext, Path terminologyPath, Path modelPath) { - IRepository data = null; - IRepository terminology = null; - - if (terminologyPath == null && modelPath == null) { - return new NoOpRepository(fhirContext); - } - - if (modelPath != null) { - data = new IgStandardRepository(fhirContext, modelPath); - } else { - data = new NoOpRepository(fhirContext); - } - - if (terminologyPath != null) { - terminology = new IgStandardRepository(fhirContext, terminologyPath); - } else { - terminology = new NoOpRepository(fhirContext); - } - - return new ProxyRepository(data, data, terminology); - } - - @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()) { - System.out.println(libraryEntry.getKey() + "=" - + this.tempConvert(libraryEntry.getValue().value())); - } - - System.out.println(); - } - - private String tempConvert(Object value) { - if (value == null) { - return "null"; - } - - String result = ""; - if (value instanceof Iterable) { - result += "["; - Iterable values = (Iterable) value; - for (Object o : values) { - result += (tempConvert(o) + ", "); - } - - if (result.length() > 1) { - result = result.substring(0, result.length() - 2); - } - - result += "]"; - } else if (value instanceof IBaseResource) { - IBaseResource resource = (IBaseResource) value; - result = resource.fhirType() - + (resource.getIdElement() != null - && resource.getIdElement().hasIdPart() - ? "(id=" + resource.getIdElement().getIdPart() + ")" - : ""); - } else if (value instanceof IBase) { - result = ((IBase) value).fhirType(); - } else if (value instanceof IBaseDatatype) { - result = ((IBaseDatatype) value).fhirType(); - } else { - result = value.toString(); - } - - return result; - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt new file mode 100644 index 00000000..7c1a1915 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt @@ -0,0 +1,280 @@ +package org.opencds.cqf.cql.ls.server.command + +import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.context.FhirVersionEnum +import ca.uhn.fhir.repository.IRepository +import kotlinx.io.files.Path +import org.cqframework.cql.cql2elm.CqlTranslatorOptions +import org.cqframework.cql.cql2elm.DefaultLibrarySourceProvider +import org.cqframework.cql.cql2elm.DefaultModelInfoProvider +import org.cqframework.fhir.npm.NpmProcessor +import org.cqframework.fhir.utilities.IGContext +import org.hl7.elm.r1.VersionedIdentifier +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.ILoggingService +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.repository.ig.standard.IgStandardRepository +import org.opencds.cqf.fhir.cql.CqlOptions +import org.opencds.cqf.fhir.cql.Engines +import org.opencds.cqf.fhir.cql.EvaluationSettings +import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings +import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.PROFILE_MODE +import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE +import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE +import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings +import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.CODE_LOOKUP_MODE +import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE +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.slf4j.LoggerFactory +import picocli.CommandLine +import picocli.CommandLine.ArgGroup +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import java.nio.file.Paths +import java.util.concurrent.Callable + +@Command(name = "cql", mixinStandardHelpOptions = true) +class CqlCommand : Callable { + + @Option(names = ["-fv", "--fhir-version"], required = true) + var fhirVersion: String = "" + + @Option(names = ["-op", "--options-path"]) + var optionsPath: String? = null + + @ArgGroup(multiplicity = "0..1", exclusive = false) + var namespace: NamespaceParameter? = null + + class NamespaceParameter { + @Option(names = ["-nn", "--namespace-name"]) + var namespaceName: String? = null + + @Option(names = ["-nu", "--namespace-uri"]) + var namespaceUri: String? = null + } + + @Option(names = ["-rd", "--root-dir"]) + var rootDir: String? = null + + @Option(names = ["-ig", "--ig-path"]) + var igPath: String? = null + + @ArgGroup(multiplicity = "1..*", exclusive = false) + var libraries: MutableList = mutableListOf() + + class LibraryParameter { + @Option(names = ["-lu", "--library-url"], required = true) + var libraryUrl: String? = null + + @Option(names = ["-ln", "--library-name"], required = true) + var libraryName: String = "" + + @Option(names = ["-lv", "--library-version"]) + var libraryVersion: String? = null + + @Option(names = ["-t", "--terminology-url"]) + var terminologyUrl: String? = null + + @ArgGroup(multiplicity = "0..1", exclusive = false) + var model: ModelParameter? = null + + @ArgGroup(multiplicity = "0..*", exclusive = false) + var parameters: MutableList = mutableListOf() + + @Option(names = ["-e", "--expression"]) + var expression: Array? = null + + @ArgGroup(multiplicity = "0..1", exclusive = false) + var context: ContextParameter? = null + + class ContextParameter { + @Option(names = ["-c", "--context"]) + var contextName: String? = null + + @Option(names = ["-cv", "--context-value"]) + var contextValue: String? = null + } + + class ModelParameter { + @Option(names = ["-m", "--model"]) + var modelName: String? = null + + @Option(names = ["-mu", "--model-url"]) + var modelUrl: String? = null + } + + class ParameterParameter { + @Option(names = ["-p", "--parameter"]) + var parameterName: String? = null + + @Option(names = ["-pv", "--parameter-value"]) + var parameterValue: String? = null + } + } + + @Suppress("removal") // TODO: Missed a spot upstream in the CQL library + private class Logger : ILoggingService { + private val log = LoggerFactory.getLogger(Logger::class.java) + + override fun logMessage(s: String) { + log.warn(s) + } + + override fun logDebugMessage(logCategory: ILoggingService.LogCategory, s: String) { + log.debug("{}: {}", logCategory, s) + } + + override fun isDebugLogging(): Boolean = log.isDebugEnabled + } + + private fun toVersionNumber(fhirVersion: FhirVersionEnum): String { + return when (fhirVersion) { + FhirVersionEnum.R4 -> "4.0.1" + FhirVersionEnum.R5 -> "5.0.0-ballot" + FhirVersionEnum.DSTU3 -> "3.0.2" + else -> throw IllegalArgumentException("Unsupported FHIR version $fhirVersion") + } + } + + @CommandLine.ParentCommand + private var parentCommand: CliCommand? = null + + override fun call(): Int { + val fhirVersionEnum = FhirVersionEnum.valueOf(fhirVersion) + val fhirContext = FhirContext.forCached(fhirVersionEnum) + + var igContext: IGContext? = null + var npmProcessor: NpmProcessor? = null + if (rootDir != null && igPath != null) { + igContext = IGContext(Logger()) + igContext.initializeFromIg(rootDir, igPath, toVersionNumber(fhirVersionEnum)) + } else if (parentCommand != null && parentCommand!!.getIgContextManager() != null && rootDir != null) { + npmProcessor = parentCommand!! + .getIgContextManager() + .getContext(Uris.addPath(Uris.addPath(java.net.URI.create(rootDir), "input")!!, "cql")!!) + if (npmProcessor != null) { + igContext = npmProcessor.igContext + } + } + + if (npmProcessor == null) { + npmProcessor = NpmProcessor(igContext) + } + + val cqlOptions = CqlOptions.defaultOptions() + + val optionsPathVal = optionsPath + if (optionsPathVal != null) { + val op = Path(Uris.parseOrNull(optionsPathVal)!!.toURL().path) + val options = CqlTranslatorOptions.fromFile(Path(op)) + cqlOptions.setCqlCompilerOptions(options.cqlCompilerOptions) + } + + val terminologySettings = TerminologySettings().apply { + setValuesetExpansionMode(VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION) + setValuesetPreExpansionMode(VALUESET_PRE_EXPANSION_MODE.USE_IF_PRESENT) + setValuesetMembershipMode(VALUESET_MEMBERSHIP_MODE.USE_EXPANSION) + setCodeLookupMode(CODE_LOOKUP_MODE.USE_CODESYSTEM_URL) + } + + val retrieveSettings = RetrieveSettings().apply { + setTerminologyParameterMode(TERMINOLOGY_FILTER_MODE.FILTER_IN_MEMORY) + setSearchParameterMode(SEARCH_FILTER_MODE.FILTER_IN_MEMORY) + setProfileMode(PROFILE_MODE.DECLARED) + } + + val evaluationSettings = EvaluationSettings.getDefault().apply { + setCqlOptions(cqlOptions) + setTerminologySettings(terminologySettings) + setRetrieveSettings(retrieveSettings) + setNpmProcessor(npmProcessor) + } + + for (library in libraries) { + // Paths are mixed types + // IgStandardRepository uses java nio path objects + // DefaultLibraryServiceProvider uses kotlin path objects + // Until the language server can be ported to kotlin, the differences will exist + val libraryUrlVal = library.libraryUrl + val libraryUri = if (libraryUrlVal != null) Uris.parseOrNull(libraryUrlVal) else null + + val libraryKotlinPath = if (libraryUri != null) Path(libraryUri.toURL().path) else null + + val modelPath = library.model?.modelUrl?.let { Paths.get(Uris.parseOrNull(it)!!) } + + val terminologyUrl = library.terminologyUrl + val terminologyPath = if (terminologyUrl != null) Paths.get(Uris.parseOrNull(terminologyUrl)!!) else null + + val repository = createRepository(fhirContext, terminologyPath, modelPath) + + val engine = Engines.forRepository(repository, evaluationSettings) + + if (library.libraryUrl != null) { + val provider = DefaultLibrarySourceProvider(libraryKotlinPath!!) + engine.environment + .libraryManager!! + .librarySourceLoader + .registerProvider(provider) + + val modelProvider = DefaultModelInfoProvider(libraryKotlinPath!!) + engine.environment + .libraryManager!! + .modelManager + .modelInfoLoader + .registerModelInfoProvider(modelProvider) + } + + val identifier = VersionedIdentifier().withId(library.libraryName) + + val contextParameter: org.apache.commons.lang3.tuple.Pair? = + if (library.context != null) { + org.apache.commons.lang3.tuple.Pair.of(library.context!!.contextName, library.context!!.contextValue as Any?) + } else null + + val result = engine.evaluate(identifier, contextParameter) + + writeResult(result) + } + + return 0 + } + + private fun createRepository(fhirContext: FhirContext, terminologyPath: java.nio.file.Path?, modelPath: java.nio.file.Path?): IRepository { + if (terminologyPath == null && modelPath == null) { + return NoOpRepository(fhirContext) + } + + val data: IRepository = if (modelPath != null) IgStandardRepository(fhirContext, modelPath) else NoOpRepository(fhirContext) + val terminology: IRepository = if (terminologyPath != null) IgStandardRepository(fhirContext, terminologyPath) else NoOpRepository(fhirContext) + + return ProxyRepository(data, data, terminology) + } + + @Suppress("java:S106") // We are intending to output to the console here as a CLI tool + private fun writeResult(result: org.opencds.cqf.cql.engine.execution.EvaluationResult) { + for ((key, value) in result.expressionResults) { + println("$key=${tempConvert(value?.value())}") + } + println() + } + + private fun tempConvert(value: Any?): String { + if (value == null) return "null" + + return when (value) { + is Iterable<*> -> { + val items = value.joinToString(", ") { tempConvert(it) } + "[$items]" + } + is IBaseResource -> value.fhirType() + + if (value.idElement != null && value.idElement.hasIdPart()) "(id=${value.idElement.idPart})" else "" + is IBase -> value.fhirType() + is IBaseDatatype -> value.fhirType() + else -> value.toString() + } + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.java deleted file mode 100644 index 3a943b51..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.opencds.cqf.cql.ls.server.command; - -import com.google.gson.JsonElement; -import java.io.ByteArrayOutputStream; -import java.io.FileDescriptor; -import java.io.FileOutputStream; -import java.io.PrintStream; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -import org.eclipse.lsp4j.ExecuteCommandParams; -import org.opencds.cqf.cql.ls.server.manager.IgContextManager; -import org.opencds.cqf.cql.ls.server.plugin.CommandContribution; -import picocli.CommandLine; - -// TODO: This will be moved to the debug plugin once that's more fully baked.. -public class DebugCqlCommandContribution implements CommandContribution { - - // TODO: Delete once the plugin is fully supported - public static final String START_DEBUG_COMMAND = "org.opencds.cqf.cql.ls.plugin.debug.startDebugSession"; - - private IgContextManager igContextManager; - - public DebugCqlCommandContribution(IgContextManager igContextManager) { - this.igContextManager = igContextManager; - } - - private CompletableFuture executeCql(ExecuteCommandParams params) { - try { - List arguments = params.getArguments().stream() - .map(JsonElement.class::cast) - .map(JsonElement::getAsString) - .collect(Collectors.toList()); - - // Temporarily redirect std out, because uh... I didn't do that very smart. - ByteArrayOutputStream baosOut = new ByteArrayOutputStream(); - System.setOut(new PrintStream(baosOut)); - - ByteArrayOutputStream baosErr = new ByteArrayOutputStream(); - System.setErr(new PrintStream(baosErr)); - - try { - CommandLine cli = new CommandLine(new CliCommand(igContextManager)); - int result = cli.execute(arguments.toArray(new String[arguments.size()])); - } catch (Exception e) { - System.err.println("Exception occurred attempting to evaluate:"); - System.err.println(e.getMessage()); - } - - String out = baosOut.toString(); - String err = baosErr.toString(); - - if (err.length() > 0) { - out += "\nEvaluation logs:\n"; - out += err; - } - - return CompletableFuture.completedFuture(out); - } finally { - System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out))); - System.setErr(new PrintStream(new FileOutputStream(FileDescriptor.err))); - } - } - - @Override - public Set getCommands() { - return Collections.singleton(START_DEBUG_COMMAND); - } - - @Override - public CompletableFuture executeCommand(ExecuteCommandParams params) { - if (START_DEBUG_COMMAND.equals(params.getCommand())) { - return this.executeCql(params); - } else { - return CommandContribution.super.executeCommand(params); - } - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.kt new file mode 100644 index 00000000..9f47370b --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.kt @@ -0,0 +1,67 @@ +package org.opencds.cqf.cql.ls.server.command + +import com.google.gson.JsonElement +import org.eclipse.lsp4j.ExecuteCommandParams +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import org.opencds.cqf.cql.ls.server.plugin.CommandContribution +import picocli.CommandLine +import java.io.ByteArrayOutputStream +import java.io.FileDescriptor +import java.io.FileOutputStream +import java.io.PrintStream +import java.util.concurrent.CompletableFuture + +// TODO: This will be moved to the debug plugin once that's more fully baked.. +class DebugCqlCommandContribution(private val igContextManager: IgContextManager) : CommandContribution { + + companion object { + // TODO: Delete once the plugin is fully supported + const val START_DEBUG_COMMAND = "org.opencds.cqf.cql.ls.plugin.debug.startDebugSession" + } + + override fun getCommands(): Set = setOf(START_DEBUG_COMMAND) + + override fun executeCommand(params: ExecuteCommandParams): CompletableFuture { + return if (START_DEBUG_COMMAND == params.command) { + executeCql(params) + } else { + super.executeCommand(params) + } + } + + private fun executeCql(params: ExecuteCommandParams): CompletableFuture { + try { + val arguments = params.arguments + .map { it as JsonElement } + .map { it.asString } + + // Temporarily redirect std out, because uh... I didn't do that very smart. + val baosOut = ByteArrayOutputStream() + System.setOut(PrintStream(baosOut)) + + val baosErr = ByteArrayOutputStream() + System.setErr(PrintStream(baosErr)) + + try { + val cli = CommandLine(CliCommand(igContextManager)) + cli.execute(*arguments.toTypedArray()) + } catch (e: Exception) { + System.err.println("Exception occurred attempting to evaluate:") + System.err.println(e.message) + } + + var out = baosOut.toString() + val err = baosErr.toString() + + if (err.isNotEmpty()) { + out += "\nEvaluation logs:\n" + out += err + } + + return CompletableFuture.completedFuture(out) + } finally { + System.setOut(PrintStream(FileOutputStream(FileDescriptor.out))) + System.setErr(PrintStream(FileOutputStream(FileDescriptor.err))) + } + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/NoOpRepository.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/NoOpRepository.java deleted file mode 100644 index f8f6fc6d..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/NoOpRepository.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.opencds.cqf.cql.ls.server.command; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.api.BundleInclusionRule; -import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.model.valueset.BundleTypeEnum; -import ca.uhn.fhir.repository.IRepository; -import ca.uhn.fhir.rest.api.MethodOutcome; -import com.google.common.collect.Multimap; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -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; -// TODO: Remove - import org.opencds.cqf.fhir.api.Repository; - -public class NoOpRepository implements IRepository { - - private final FhirContext fhirContext; - - public NoOpRepository(FhirContext fhirContext) { - this.fhirContext = fhirContext; - } - - @Override - public T read(Class aClass, I i, Map map) { - throw new UnsupportedOperationException("Unimplemented method 'read'"); - } - - @Override - public MethodOutcome create(T t, Map map) { - throw new UnsupportedOperationException("Unimplemented method 'create'"); - } - - @Override - public MethodOutcome update(T t, Map map) { - throw new UnsupportedOperationException("Unimplemented method 'update'"); - } - - @Override - public MethodOutcome delete( - Class aClass, I i, Map map) { - throw new UnsupportedOperationException("Unimplemented method 'delete'"); - } - - @SuppressWarnings("unchecked") - @Override - public B search( - Class aClass, - Class aClass1, - Multimap> multimap, - Map map) { - var factory = this.fhirContext.newBundleFactory(); - factory.addResourcesToBundle( - Collections.emptyList(), BundleTypeEnum.SEARCHSET, "", BundleInclusionRule.BASED_ON_INCLUDES, Set.of()); - return (B) factory.getResourceBundle(); - } - - @Override - public R invoke( - Class aClass, String s, P p, Class aClass1, Map map) { - throw new UnsupportedOperationException("Unimplemented method 'invoke'"); - } - - @Override - public R invoke( - I i, String s, P p, Class aClass, Map map) { - throw new UnsupportedOperationException("Unimplemented method 'invoke'"); - } - - @Override - public FhirContext fhirContext() { - return this.fhirContext; - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/NoOpRepository.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/NoOpRepository.kt new file mode 100644 index 00000000..94388a1a --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/NoOpRepository.kt @@ -0,0 +1,61 @@ +package org.opencds.cqf.cql.ls.server.command + +import ca.uhn.fhir.context.FhirContext +import ca.uhn.fhir.context.api.BundleInclusionRule +import ca.uhn.fhir.model.api.IQueryParameterType +import ca.uhn.fhir.model.valueset.BundleTypeEnum +import ca.uhn.fhir.repository.IRepository +import ca.uhn.fhir.rest.api.MethodOutcome +import com.google.common.collect.Multimap +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 java.util.Collections + +class NoOpRepository(private val fhirContext: FhirContext) : IRepository { + + override fun read(aClass: Class, i: I, map: Map): T { + throw UnsupportedOperationException("Unimplemented method 'read'") + } + + override fun create(t: T, map: Map): MethodOutcome { + throw UnsupportedOperationException("Unimplemented method 'create'") + } + + override fun update(t: T, map: Map): MethodOutcome { + throw UnsupportedOperationException("Unimplemented method 'update'") + } + + override fun delete(aClass: Class, i: I, map: Map): MethodOutcome { + throw UnsupportedOperationException("Unimplemented method 'delete'") + } + + @Suppress("UNCHECKED_CAST") + override fun search( + aClass: Class, + aClass1: Class, + multimap: Multimap>, + map: Map + ): B { + val factory = fhirContext.newBundleFactory() + factory.addResourcesToBundle( + Collections.emptyList(), BundleTypeEnum.SEARCHSET, "", BundleInclusionRule.BASED_ON_INCLUDES, emptySet() + ) + return factory.resourceBundle as B + } + + override fun invoke( + aClass: Class, s: String, p: P, aClass1: Class, map: Map + ): R { + throw UnsupportedOperationException("Unimplemented method 'invoke'") + } + + override fun invoke( + i: I, s: String, p: P, aClass: Class, map: Map + ): R { + throw UnsupportedOperationException("Unimplemented method 'invoke'") + } + + override fun fhirContext(): FhirContext = fhirContext +} 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 deleted file mode 100644 index f682b816..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.opencds.cqf.cql.ls.server.command; - -import com.google.gson.JsonElement; -import java.io.IOException; -import java.net.URI; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import org.cqframework.cql.cql2elm.CqlCompiler; -import org.cqframework.cql.elm.serializing.ElmJsonLibraryWriter; -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; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.plugin.CommandContribution; - -public class ViewElmCommandContribution implements CommandContribution { - private static final String VIEW_ELM_COMMAND = "org.opencds.cqf.cql.ls.viewElm"; - - private final CqlCompilationManager cqlCompilationManager; - - public ViewElmCommandContribution(CqlCompilationManager cqlCompilationManager) { - this.cqlCompilationManager = cqlCompilationManager; - } - - @Override - public Set getCommands() { - return Collections.singleton(VIEW_ELM_COMMAND); - } - - @Override - public CompletableFuture executeCommand(ExecuteCommandParams params) { - switch (params.getCommand()) { - case VIEW_ELM_COMMAND: - return this.viewElm(params); - default: - return CommandContribution.super.executeCommand(params); - } - } - - // There's currently not a "show text file" or similar command in the LSP spec, - // So it's not client agnostic. The client has to know that the result of this - // command - // is XML and display it accordingly. - private CompletableFuture viewElm(ExecuteCommandParams params) { - List args = params.getArguments(); - - // Defensive check: ensure we have at least the URI - if (args == null || args.isEmpty()) { - return CompletableFuture.completedFuture(null); - } - - String uriString = ((JsonElement) args.get(0)).getAsString(); - - // Handle missing or null elmType by defaulting to "xml" - String elmType = "xml"; - if (args.size() > 1 && args.get(1) != null) { - elmType = ((JsonElement) args.get(1)).getAsString(); - } - - try { - URI uri = Uris.parseOrNull(uriString); - CqlCompiler compiler = this.cqlCompilationManager.compile(uri); - - if (compiler != null) { - // Use .equalsIgnoreCase for better robustness - if ("xml".equalsIgnoreCase(elmType)) { - return CompletableFuture.completedFuture(convertToXml(compiler.getLibrary())); - } else { - return CompletableFuture.completedFuture(convertToJson(compiler.getLibrary())); - } - } - - return CompletableFuture.completedFuture(null); - } catch (Exception e) { - // Log the error here if possible to avoid "silent" failures - return CompletableFuture.completedFuture(null); - } - } - - private static String convertToXml(Library library) throws IOException { - ElmXmlLibraryWriter writer = new ElmXmlLibraryWriter(); - return writer.writeAsString(library); - } - - private static String convertToJson(Library library) throws IOException { - ElmJsonLibraryWriter writer = new ElmJsonLibraryWriter(); - return writer.writeAsString(library); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt new file mode 100644 index 00000000..40afacd0 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt @@ -0,0 +1,71 @@ +package org.opencds.cqf.cql.ls.server.command + +import com.google.gson.JsonElement +import org.cqframework.cql.elm.serializing.ElmJsonLibraryWriter +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 +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.plugin.CommandContribution +import java.util.concurrent.CompletableFuture + +class ViewElmCommandContribution(private val cqlCompilationManager: CqlCompilationManager) : CommandContribution { + + companion object { + private const val VIEW_ELM_COMMAND = "org.opencds.cqf.cql.ls.viewElm" + } + + override fun getCommands(): Set = setOf(VIEW_ELM_COMMAND) + + override fun executeCommand(params: ExecuteCommandParams): CompletableFuture { + return when (params.command) { + VIEW_ELM_COMMAND -> viewElm(params) + else -> super.executeCommand(params) + } + } + + // There's currently not a "show text file" or similar command in the LSP spec, + // So it's not client agnostic. The client has to know that the result of this + // command is XML and display it accordingly. + private fun viewElm(params: ExecuteCommandParams): CompletableFuture { + val args = params.arguments + + // Defensive check: ensure we have at least the URI + if (args == null || args.isEmpty()) { + return CompletableFuture.completedFuture(null) + } + + val uriString = (args[0] as JsonElement).asString + + // Handle missing or null elmType by defaulting to "xml" + val elmType = if (args.size > 1 && args[1] != null) { + (args[1] as JsonElement).asString + } else { + "xml" + } + + return try { + val uri = Uris.parseOrNull(uriString) + val compiler = cqlCompilationManager.compile(uri!!) + + if (compiler != null) { + // Use equalsIgnoreCase for better robustness + if (elmType.equals("xml", ignoreCase = true)) { + CompletableFuture.completedFuture(convertToXml(compiler.library!!)) + } else { + CompletableFuture.completedFuture(convertToJson(compiler.library!!)) + } + } else { + CompletableFuture.completedFuture(null) + } + } catch (e: Exception) { + // Log the error here if possible to avoid "silent" failures + CompletableFuture.completedFuture(null) + } + } + + private fun convertToXml(library: Library): String = ElmXmlLibraryWriter().writeAsString(library) + + private fun convertToJson(library: Library): String = ElmJsonLibraryWriter().writeAsString(library) +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/config/PluginConfig.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/config/PluginConfig.java deleted file mode 100644 index 1875cef6..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/config/PluginConfig.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.opencds.cqf.cql.ls.server.config; - -import java.util.ArrayList; -import java.util.List; -import java.util.ServiceLoader; -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.services.LanguageClient; -import org.opencds.cqf.cql.ls.server.command.DebugCqlCommandContribution; -import org.opencds.cqf.cql.ls.server.command.ViewElmCommandContribution; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.manager.IgContextManager; -import org.opencds.cqf.cql.ls.server.plugin.CommandContribution; -import org.opencds.cqf.cql.ls.server.plugin.CqlLanguageServerPlugin; -import org.opencds.cqf.cql.ls.server.plugin.CqlLanguageServerPluginFactory; -import org.opencds.cqf.cql.ls.server.service.CqlTextDocumentService; -import org.opencds.cqf.cql.ls.server.service.CqlWorkspaceService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class PluginConfig { - - // The indirection here is to break the cycle between the workspace service and the plugin - // contributions - @Bean - CompletableFuture> futureCommandContributions() { - return new CompletableFuture<>(); - } - - @Bean - public List pluginCommandContributions( - CompletableFuture client, - CqlWorkspaceService cqlWorkspaceService, - CqlTextDocumentService cqlTextDocumentService, - CqlCompilationManager cqlCompilationManager, - IgContextManager igContextManager, - CompletableFuture> futureCommandContributions) { - - ServiceLoader pluginFactories = - ServiceLoader.load(CqlLanguageServerPluginFactory.class); - - List pluginCommandContributions = new ArrayList<>(); - - for (CqlLanguageServerPluginFactory pluginFactory : pluginFactories) { - CqlLanguageServerPlugin plugin = pluginFactory.createPlugin( - client, cqlWorkspaceService, cqlTextDocumentService, cqlCompilationManager); - if (plugin.getCommandContribution() != null) { - pluginCommandContributions.add(plugin.getCommandContribution()); - } - } - - pluginCommandContributions.add(new ViewElmCommandContribution(cqlCompilationManager)); - pluginCommandContributions.add(new DebugCqlCommandContribution(igContextManager)); - - futureCommandContributions.complete(pluginCommandContributions); - - return pluginCommandContributions; - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/config/ServerConfig.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/config/ServerConfig.java deleted file mode 100644 index d71aeb7d..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/config/ServerConfig.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.opencds.cqf.cql.ls.server.config; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.WorkspaceFolder; -import org.eclipse.lsp4j.services.LanguageClient; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Logger.JavaLogger; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.server.CqlLanguageServer; -import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.manager.IgContextManager; -import org.opencds.cqf.cql.ls.server.plugin.CommandContribution; -import org.opencds.cqf.cql.ls.server.provider.FormattingProvider; -import org.opencds.cqf.cql.ls.server.provider.HoverProvider; -import org.opencds.cqf.cql.ls.server.service.*; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -@Configuration -@Import(PluginConfig.class) -public class ServerConfig { - - @Bean(name = "fileContentService") - public ContentService fileContentService(List workspaceFolders) { - return new FileContentService(workspaceFolders); - } - - @Bean(name = {"activeContentService"}) - public ActiveContentService activeContentService(EventBus eventBus) { - ActiveContentService ac = new ActiveContentService(); - - eventBus.register(ac); - - return ac; - } - - @Bean(name = {"federatedContentService"}) - public FederatedContentService federatedContentService( - ActiveContentService activeContentService, - @Qualifier("fileContentService") ContentService fileContentService) { - return new FederatedContentService(activeContentService, fileContentService); - } - - @Bean - public CqlLanguageServer cqlLanguageServer( - CompletableFuture languageClient, - CqlWorkspaceService cqlWorkspaceService, - CqlTextDocumentService cqlTextDocumentService) { - return new CqlLanguageServer(languageClient, cqlWorkspaceService, cqlTextDocumentService); - } - - @Bean - public List workspaceFolders() { - return new ArrayList<>(); - } - - @Bean - public CompletableFuture languageClient() { - return new CompletableFuture<>(); - } - - @Bean - public CqlTextDocumentService cqlTextDocumentService( - CompletableFuture languageClient, - HoverProvider hoverProvider, - FormattingProvider formattingProvider, - EventBus eventBus) { - return new CqlTextDocumentService(languageClient, hoverProvider, formattingProvider, eventBus); - } - - @Bean - CqlWorkspaceService cqlWorkspaceService( - CompletableFuture languageClient, - CompletableFuture> commandContributions, - List workspaceFolders, - EventBus eventBus) { - return new CqlWorkspaceService(languageClient, commandContributions, workspaceFolders, eventBus); - } - - @Bean - CompilerOptionsManager compilerOptionsManager( - @Qualifier("federatedContentService") ContentService federatedContentService, EventBus eventBus) { - CompilerOptionsManager t = new CompilerOptionsManager(federatedContentService); - - eventBus.register(t); - - return t; - } - - @Bean - IgContextManager igContextManager( - @Qualifier("federatedContentService") ContentService contentService, EventBus eventBus) { - IgContextManager i = new IgContextManager(contentService); - - eventBus.register(i); - - return i; - } - - @Bean - CqlCompilationManager cqlCompilationManager( - FederatedContentService federatedContentService, - CompilerOptionsManager compilerOptionsManager, - IgContextManager igContextManager) { - return new CqlCompilationManager(federatedContentService, compilerOptionsManager, igContextManager); - } - - @Bean - HoverProvider hoverProvider(CqlCompilationManager cqlCompilationManager) { - return new HoverProvider(cqlCompilationManager); - } - - @Bean - FormattingProvider formattingProvider(FederatedContentService contentService) { - return new FormattingProvider(contentService); - } - - @Bean - DiagnosticsService diagnosticsService( - CompletableFuture languageClient, - CqlCompilationManager cqlCompilationManager, - FederatedContentService contentService, - EventBus eventBus) { - DiagnosticsService ds = new DiagnosticsService(languageClient, cqlCompilationManager, contentService); - - eventBus.register(ds); - - return ds; - } - - @Bean - EventBus eventBus() { - return EventBus.builder().logger(new JavaLogger("eventBus")).build(); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/BaseEvent.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/BaseEvent.java deleted file mode 100644 index ff61e34c..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/BaseEvent.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.opencds.cqf.cql.ls.server.event; - -abstract class BaseEvent { - - private T params; - - protected BaseEvent(T params) { - this.params = params; - } - - public T params() { - return this.params; - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/BaseEvent.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/BaseEvent.kt new file mode 100644 index 00000000..51e6c499 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/BaseEvent.kt @@ -0,0 +1,5 @@ +package org.opencds.cqf.cql.ls.server.event + +abstract class BaseEvent(private val params: T) { + fun params(): T = params +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeTextDocumentEvent.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeTextDocumentEvent.java deleted file mode 100644 index 7d1fc47d..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeTextDocumentEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.opencds.cqf.cql.ls.server.event; - -import org.eclipse.lsp4j.DidChangeTextDocumentParams; - -public class DidChangeTextDocumentEvent extends BaseEvent { - public DidChangeTextDocumentEvent(DidChangeTextDocumentParams params) { - super(params); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeTextDocumentEvent.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeTextDocumentEvent.kt new file mode 100644 index 00000000..f952a0e4 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeTextDocumentEvent.kt @@ -0,0 +1,5 @@ +package org.opencds.cqf.cql.ls.server.event + +import org.eclipse.lsp4j.DidChangeTextDocumentParams + +class DidChangeTextDocumentEvent(params: DidChangeTextDocumentParams) : BaseEvent(params) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeWatchedFilesEvent.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeWatchedFilesEvent.java deleted file mode 100644 index 9b9cf2de..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeWatchedFilesEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.opencds.cqf.cql.ls.server.event; - -import org.eclipse.lsp4j.DidChangeWatchedFilesParams; - -public class DidChangeWatchedFilesEvent extends BaseEvent { - public DidChangeWatchedFilesEvent(DidChangeWatchedFilesParams params) { - super(params); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeWatchedFilesEvent.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeWatchedFilesEvent.kt new file mode 100644 index 00000000..31ffc440 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidChangeWatchedFilesEvent.kt @@ -0,0 +1,5 @@ +package org.opencds.cqf.cql.ls.server.event + +import org.eclipse.lsp4j.DidChangeWatchedFilesParams + +class DidChangeWatchedFilesEvent(params: DidChangeWatchedFilesParams) : BaseEvent(params) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidCloseTextDocumentEvent.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidCloseTextDocumentEvent.java deleted file mode 100644 index 00ceaa1e..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidCloseTextDocumentEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.opencds.cqf.cql.ls.server.event; - -import org.eclipse.lsp4j.DidCloseTextDocumentParams; - -public class DidCloseTextDocumentEvent extends BaseEvent { - public DidCloseTextDocumentEvent(DidCloseTextDocumentParams params) { - super(params); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidCloseTextDocumentEvent.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidCloseTextDocumentEvent.kt new file mode 100644 index 00000000..5441737e --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidCloseTextDocumentEvent.kt @@ -0,0 +1,5 @@ +package org.opencds.cqf.cql.ls.server.event + +import org.eclipse.lsp4j.DidCloseTextDocumentParams + +class DidCloseTextDocumentEvent(params: DidCloseTextDocumentParams) : BaseEvent(params) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidOpenTextDocumentEvent.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidOpenTextDocumentEvent.java deleted file mode 100644 index 2e718fae..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidOpenTextDocumentEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.opencds.cqf.cql.ls.server.event; - -import org.eclipse.lsp4j.DidOpenTextDocumentParams; - -public class DidOpenTextDocumentEvent extends BaseEvent { - public DidOpenTextDocumentEvent(DidOpenTextDocumentParams params) { - super(params); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidOpenTextDocumentEvent.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidOpenTextDocumentEvent.kt new file mode 100644 index 00000000..9ffc4d5f --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidOpenTextDocumentEvent.kt @@ -0,0 +1,5 @@ +package org.opencds.cqf.cql.ls.server.event + +import org.eclipse.lsp4j.DidOpenTextDocumentParams + +class DidOpenTextDocumentEvent(params: DidOpenTextDocumentParams) : BaseEvent(params) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidSaveTextDocumentEvent.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidSaveTextDocumentEvent.java deleted file mode 100644 index 571e9a28..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidSaveTextDocumentEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.opencds.cqf.cql.ls.server.event; - -import org.eclipse.lsp4j.DidSaveTextDocumentParams; - -public class DidSaveTextDocumentEvent extends BaseEvent { - public DidSaveTextDocumentEvent(DidSaveTextDocumentParams params) { - super(params); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidSaveTextDocumentEvent.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidSaveTextDocumentEvent.kt new file mode 100644 index 00000000..e56d9b2a --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/event/DidSaveTextDocumentEvent.kt @@ -0,0 +1,5 @@ +package org.opencds.cqf.cql.ls.server.event + +import org.eclipse.lsp4j.DidSaveTextDocumentParams + +class DidSaveTextDocumentEvent(params: DidSaveTextDocumentParams) : BaseEvent(params) 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 deleted file mode 100644 index ef552fcb..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.opencds.cqf.cql.ls.server.manager; - -import static kotlinx.io.files.PathsKt.Path; - -import java.io.InputStream; -import java.net.URI; -import java.util.HashMap; -import java.util.Map; -import org.cqframework.cql.cql2elm.CqlCompilerOptions; -import org.cqframework.cql.cql2elm.CqlTranslatorOptions; -import org.cqframework.cql.cql2elm.LibraryBuilder.SignatureLevel; -import org.eclipse.lsp4j.FileEvent; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.core.utility.Uris; -import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CompilerOptionsManager { - - private static final Logger log = LoggerFactory.getLogger(CompilerOptionsManager.class); - - private ContentService contentService; - - public CompilerOptionsManager(ContentService contentService) { - this.contentService = contentService; - } - - private final Map cachedOptions = new HashMap<>(); - - public CqlCompilerOptions getOptions(URI uri) { - URI root = Uris.getHead(uri); - return cachedOptions.computeIfAbsent(root, this::readOptions); - } - - protected void clearOptions(URI uri) { - URI root = Uris.getHead(uri); - this.cachedOptions.remove(root); - } - - protected CqlCompilerOptions readOptions(URI rootUri) { - CqlCompilerOptions options = null; - - var optionsUri = Uris.addPath(rootUri, "/cql/cql-options.json"); - InputStream input = contentService.read(optionsUri); - - if (input != null) { - try { - options = CqlTranslatorOptions.fromFile(Path(optionsUri.toURL().getPath())) - .getCqlCompilerOptions(); - } catch (Exception e) { - log.info(String.format( - "Exception %s attempting to load options from %s, using default options", - e.getMessage(), optionsUri.toString())); - } - } else { - log.info(String.format("%s not found, using default options", optionsUri.toString())); - options = CqlTranslatorOptions.defaultOptions().getCqlCompilerOptions(); - } - - // For the purposes of debugging and authoring support, always add detailed - // translation information. - return options.withOptions( - CqlCompilerOptions.Options.EnableLocators, - CqlCompilerOptions.Options.EnableResultTypes, - CqlCompilerOptions.Options.EnableAnnotations) - .withSignatureLevel(SignatureLevel.All); - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - public void onMessageEvent(DidChangeWatchedFilesEvent event) { - for (FileEvent e : event.params().getChanges()) { - if (e.getUri().endsWith("cql-options.json")) { - this.clearOptions(Uris.parseOrNull(e.getUri())); - } - } - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt new file mode 100644 index 00000000..c3e61eb0 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt @@ -0,0 +1,71 @@ +package org.opencds.cqf.cql.ls.server.manager + +import kotlinx.io.files.Path +import org.cqframework.cql.cql2elm.CqlCompilerOptions +import org.cqframework.cql.cql2elm.CqlTranslatorOptions +import org.cqframework.cql.cql2elm.LibraryBuilder.SignatureLevel +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent +import org.slf4j.LoggerFactory +import java.net.URI + +class CompilerOptionsManager(private val contentService: ContentService) { + + companion object { + private val log = LoggerFactory.getLogger(CompilerOptionsManager::class.java) + } + + private val cachedOptions = HashMap() + + fun getOptions(uri: URI): CqlCompilerOptions { + val root = Uris.getHead(uri) + return cachedOptions.getOrPut(root) { readOptions(root) } + } + + internal fun clearOptions(uri: URI) { + val root = Uris.getHead(uri) + cachedOptions.remove(root) + } + + protected fun readOptions(rootUri: URI): CqlCompilerOptions { + var options: CqlCompilerOptions? + + val optionsUri = Uris.addPath(rootUri, "/cql/cql-options.json") + val input = contentService.read(optionsUri!!) + + options = if (input != null) { + try { + CqlTranslatorOptions.fromFile(Path(optionsUri.toURL().path)) + .cqlCompilerOptions + } catch (e: Exception) { + log.info("Exception ${e.message} attempting to load options from $optionsUri, using default options") + null + } + } else { + log.info("$optionsUri not found, using default options") + null + } + + if (options == null) { + options = CqlTranslatorOptions.defaultOptions().cqlCompilerOptions + } + + return options!!.withOptions( + CqlCompilerOptions.Options.EnableLocators, + CqlCompilerOptions.Options.EnableResultTypes, + CqlCompilerOptions.Options.EnableAnnotations + ).withSignatureLevel(SignatureLevel.All) + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + fun onMessageEvent(event: DidChangeWatchedFilesEvent) { + for (e in event.params().changes) { + if (e.uri.endsWith("cql-options.json")) { + clearOptions(Uris.parseOrNull(e.uri)!!) + } + } + } +} 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 deleted file mode 100644 index 82a6f5c1..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManager.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.opencds.cqf.cql.ls.server.manager; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.HashMap; -import java.util.Map; -import org.cqframework.cql.cql2elm.CqlCompiler; -import org.cqframework.cql.cql2elm.LibraryManager; -import org.cqframework.cql.cql2elm.ModelManager; -import org.cqframework.cql.cql2elm.model.Model; -import org.cqframework.cql.cql2elm.quick.FhirLibrarySourceProvider; -import org.fhir.ucum.UcumEssenceService; -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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CqlCompilationManager { - private static final Logger log = LoggerFactory.getLogger(CqlCompilationManager.class); - - private final Map globalCache; - private final ContentService contentService; - private static UcumService ucumService = null; - private final CompilerOptionsManager compilerOptionsManager; - - private final IgContextManager igContextManager; - - static { - try { - ucumService = new UcumEssenceService(UcumEssenceService.class.getResourceAsStream("/ucum-essence.xml")); - } catch (Exception e) { - log.warn("error initializing UcumService", e); - } - } - - public CqlCompilationManager( - ContentService contentService, - CompilerOptionsManager compilerOptionsManager, - IgContextManager igContextManager) { - this.globalCache = new HashMap<>(); - this.contentService = contentService; - this.compilerOptionsManager = compilerOptionsManager; - this.igContextManager = igContextManager; - } - - private synchronized IgContextManager getIgContextManager() { - return this.igContextManager; - } - - public CqlCompiler compile(URI uri) { - InputStream input = contentService.read(uri); - if (input == null) { - return null; - } - - return this.compile(uri, input); - } - - public CqlCompiler compile(URI uri, InputStream stream) { - ModelManager modelManager = this.createModelManager(); - - LibraryManager libraryManager = this.createLibraryManager(Uris.getHead(uri), modelManager); - - try { - 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); - } - } - - private ModelManager createModelManager() { - return new ModelManager(this.globalCache); - } - - private LibraryManager createLibraryManager(URI root, ModelManager modelManager) { - // TODO: Build a manager similar CompilerOptionsManager to support reacting to modelInfo file changes - modelManager - .getModelInfoLoader() - .registerModelInfoProvider(new ContentServiceModelInfoProvider(root, this.contentService)); - LibraryManager libraryManager = new LibraryManager(modelManager, this.compilerOptionsManager.getOptions(root)); - libraryManager - .getLibrarySourceLoader() - .registerProvider(new ContentServiceSourceProvider(root, this.contentService)); - libraryManager.getLibrarySourceLoader().registerProvider(new FhirLibrarySourceProvider()); - getIgContextManager().setupLibraryManager(root, libraryManager); - return libraryManager; - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManager.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManager.kt new file mode 100644 index 00000000..85ad97c1 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManager.kt @@ -0,0 +1,71 @@ +package org.opencds.cqf.cql.ls.server.manager + +import org.cqframework.cql.cql2elm.CqlCompiler +import org.cqframework.cql.cql2elm.LibraryManager +import org.cqframework.cql.cql2elm.ModelManager +import org.cqframework.cql.cql2elm.model.Model +import org.cqframework.cql.cql2elm.quick.FhirLibrarySourceProvider +import org.fhir.ucum.UcumEssenceService +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 +import org.slf4j.LoggerFactory +import java.io.InputStream +import java.net.URI + +class CqlCompilationManager( + private val contentService: ContentService, + private val compilerOptionsManager: CompilerOptionsManager, + private val igContextManager: IgContextManager +) { + companion object { + private val log = LoggerFactory.getLogger(CqlCompilationManager::class.java) + private var ucumService: UcumService? = null + + init { + try { + ucumService = UcumEssenceService( + UcumEssenceService::class.java.getResourceAsStream("/ucum-essence.xml") + ) + } catch (e: Exception) { + log.warn("error initializing UcumService", e) + } + } + } + + private val globalCache = HashMap() + + private fun getIgContextManager(): IgContextManager = igContextManager + + fun compile(uri: URI): CqlCompiler? { + val input = contentService.read(uri) ?: return null + return compile(uri, input) + } + + fun compile(uri: URI, stream: InputStream): CqlCompiler { + val modelManager = createModelManager() + val libraryManager = createLibraryManager(Uris.getHead(uri), modelManager) + val compiler = CqlCompiler(null, null, libraryManager) + compiler.run(Converters.inputStreamToString(stream)) + return compiler + } + + private fun createModelManager() = ModelManager(globalCache) + + private fun createLibraryManager(root: URI, modelManager: ModelManager): LibraryManager { + modelManager.modelInfoLoader.registerModelInfoProvider( + ContentServiceModelInfoProvider(root, contentService) + ) + val libraryManager = LibraryManager(modelManager, compilerOptionsManager.getOptions(root)) + libraryManager.librarySourceLoader.registerProvider( + ContentServiceSourceProvider(root, contentService) + ) + libraryManager.librarySourceLoader.registerProvider(FhirLibrarySourceProvider()) + getIgContextManager().setupLibraryManager(root, libraryManager) + return libraryManager + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.java deleted file mode 100644 index 5af30f31..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.opencds.cqf.cql.ls.server.manager; - -import java.io.InputStream; -import java.net.URI; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import org.cqframework.cql.cql2elm.LibraryManager; -import org.cqframework.fhir.npm.ILibraryReader; -import org.cqframework.fhir.npm.NpmLibrarySourceProvider; -import org.cqframework.fhir.npm.NpmModelInfoProvider; -import org.cqframework.fhir.npm.NpmProcessor; -import org.cqframework.fhir.utilities.IGContext; -import org.cqframework.fhir.utilities.LoggerAdapter; -import org.eclipse.lsp4j.FileEvent; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.core.utility.Uris; -import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class IgContextManager { - private static final Logger log = LoggerFactory.getLogger(IgContextManager.class); - - private ContentService contentService; - - public IgContextManager(ContentService contentService) { - this.contentService = contentService; - } - - private final Map> cachedContext = new ConcurrentHashMap<>(); - - public NpmProcessor getContext(URI uri) { - URI root = Uris.getHead(uri); - return cachedContext.computeIfAbsent(root, this::readContext).orElse(null); - } - - protected void clearContext(URI uri) { - URI root = Uris.getHead(uri); - this.cachedContext.remove(root); - } - - protected Optional readContext(URI rootUri) { - IGContext igContext = findIgContext(rootUri); - if (igContext != null) { - return Optional.of(new NpmProcessor(igContext)); - } - - return Optional.empty(); - } - - public synchronized void setupLibraryManager(URI uri, LibraryManager libraryManager) { - NpmProcessor npmProcessor = getContext(uri); - if (npmProcessor != null) { - var namespaceManager = libraryManager.getNamespaceManager(); - namespaceManager.ensureNamespaceRegistered(npmProcessor.getIgNamespace()); - ILibraryReader reader = new org.cqframework.fhir.npm.LibraryLoader( - npmProcessor.getIgContext().getFhirVersion()); - LoggerAdapter adapter = new LoggerAdapter(log); - libraryManager - .getLibrarySourceLoader() - .registerProvider(new NpmLibrarySourceProvider( - npmProcessor.getPackageManager().getNpmList(), reader, adapter)); - libraryManager - .getModelManager() - .getModelInfoLoader() - .registerModelInfoProvider(new NpmModelInfoProvider( - npmProcessor.getPackageManager().getNpmList(), reader, adapter)); - - // TODO: This is a workaround for: a) multiple packages with the same package id will be in the dependency - // list, and b) there are packages with different package ids but the same base canonical (e.g. - // fhir.r4.examples has the same base canonical as fhir.r4) - // NOTE: Using ensureNamespaceRegistered works around a but not b - // NOTE: This logic is also used in org.opencds.cqf.fhir.cql.Engines.buildEnvironment() - Set keys = new HashSet(); - Set uris = new HashSet(); - for (var n : npmProcessor.getNamespaces()) { - if (!keys.contains(n.getName()) && !uris.contains(n.getUri())) { - libraryManager.getNamespaceManager().addNamespace(n); - keys.add(n.getName()); - uris.add(n.getUri()); - } - } - } - } - - /** - * Searches for an ig.ini file in the parent and grandparent of the given uri - * - * @param uri - * @return - */ - protected IGContext findIgContext(URI uri) { - // TODO: Support igs that don't have an ini by just looking for an implementation guide - // resource - log.info("Searching for ini file in {}", uri); - URI current = uri; - for (int i = 0; i < 2; i++) { - URI parent = Uris.getHead(current); - if (!parent.equals(current)) { - current = parent; - URI igIniPath = Uris.addPath(parent, "/ig.ini"); - log.info("Attempting to read ini from path {}", igIniPath); - InputStream input = contentService.read(igIniPath); - if (input != null) { - log.info("Initializing ig from ini..."); - IGContext igContext = new IGContext(new LoggerAdapter(log)); - igContext.initializeFromIni(igIniPath.getSchemeSpecificPart()); - log.info("IGContext Initialized."); - return igContext; - } - } - } - return null; - } - - @Subscribe(threadMode = ThreadMode.ASYNC) - public void onMessageEvent(DidChangeWatchedFilesEvent event) { - for (FileEvent e : event.params().getChanges()) { - if (e.getUri().endsWith("ig.ini")) { - this.clearContext(Uris.parseOrNull(e.getUri())); - } - } - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt new file mode 100644 index 00000000..01d7d926 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt @@ -0,0 +1,101 @@ +package org.opencds.cqf.cql.ls.server.manager + +import org.cqframework.fhir.npm.ILibraryReader +import org.cqframework.fhir.npm.NpmLibrarySourceProvider +import org.cqframework.fhir.npm.NpmModelInfoProvider +import org.cqframework.fhir.npm.NpmProcessor +import org.cqframework.fhir.utilities.IGContext +import org.cqframework.fhir.utilities.LoggerAdapter +import org.cqframework.cql.cql2elm.LibraryManager +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent +import org.slf4j.LoggerFactory +import java.net.URI +import java.util.Optional +import java.util.concurrent.ConcurrentHashMap + +class IgContextManager(private val contentService: ContentService) { + + companion object { + private val log = LoggerFactory.getLogger(IgContextManager::class.java) + } + + private val cachedContext = ConcurrentHashMap>() + + fun getContext(uri: URI): NpmProcessor? { + val root = Uris.getHead(uri) + return cachedContext.getOrPut(root) { readContext(root) }.orElse(null) + } + + protected fun clearContext(uri: URI) { + val root = Uris.getHead(uri) + cachedContext.remove(root) + } + + protected fun readContext(rootUri: URI): Optional { + val igContext = findIgContext(rootUri) + return if (igContext != null) Optional.of(NpmProcessor(igContext)) else Optional.empty() + } + + @Synchronized + fun setupLibraryManager(uri: URI, libraryManager: LibraryManager) { + val npmProcessor = getContext(uri) ?: return + val namespaceManager = libraryManager.namespaceManager + npmProcessor.igNamespace?.let { namespaceManager.ensureNamespaceRegistered(it) } + val reader: ILibraryReader = org.cqframework.fhir.npm.LibraryLoader( + npmProcessor.igContext!!.fhirVersion + ) + val adapter = LoggerAdapter(log) + val npmList = npmProcessor.getPackageManager().npmList + libraryManager.librarySourceLoader.registerProvider( + NpmLibrarySourceProvider(npmList, reader, adapter) + ) + libraryManager.modelManager.modelInfoLoader.registerModelInfoProvider( + NpmModelInfoProvider(npmList, reader, adapter) + ) + + val keys = mutableSetOf() + val uris = mutableSetOf() + for (n in npmProcessor.namespaces) { + if (!keys.contains(n.name) && !uris.contains(n.uri)) { + libraryManager.namespaceManager.addNamespace(n) + keys.add(n.name) + uris.add(n.uri) + } + } + } + + protected fun findIgContext(uri: URI): IGContext? { + log.info("Searching for ini file in {}", uri) + var current = uri + for (i in 0 until 2) { + val parent = Uris.getHead(current) + if (parent != current) { + current = parent + val igIniPath = Uris.addPath(parent, "/ig.ini")!! + log.info("Attempting to read ini from path {}", igIniPath) + val input = contentService.read(igIniPath) + if (input != null) { + log.info("Initializing ig from ini...") + val igContext = IGContext(LoggerAdapter(log)) + igContext.initializeFromIni(igIniPath.schemeSpecificPart) + log.info("IGContext Initialized.") + return igContext + } + } + } + return null + } + + @Subscribe(threadMode = ThreadMode.ASYNC) + fun onMessageEvent(event: DidChangeWatchedFilesEvent) { + for (e in event.params().changes) { + if (e.uri.endsWith("ig.ini")) { + clearContext(Uris.parseOrNull(e.uri)!!) + } + } + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CommandContribution.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CommandContribution.java deleted file mode 100644 index 31a15a18..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CommandContribution.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.opencds.cqf.cql.ls.server.plugin; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.ExecuteCommandParams; - -public interface CommandContribution { - default Set getCommands() { - return Collections.emptySet(); - } - - default CompletableFuture executeCommand(ExecuteCommandParams params) { - throw new RuntimeException(String.format("Unsupported Command: %s", params.getCommand())); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CommandContribution.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CommandContribution.kt new file mode 100644 index 00000000..4e117911 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CommandContribution.kt @@ -0,0 +1,12 @@ +package org.opencds.cqf.cql.ls.server.plugin + +import org.eclipse.lsp4j.ExecuteCommandParams +import java.util.concurrent.CompletableFuture + +interface CommandContribution { + fun getCommands(): Set = emptySet() + + fun executeCommand(params: ExecuteCommandParams): CompletableFuture { + throw RuntimeException("Unsupported Command: ${params.command}") + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPlugin.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPlugin.java deleted file mode 100644 index 7de5bca1..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPlugin.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.opencds.cqf.cql.ls.server.plugin; - -public interface CqlLanguageServerPlugin { - String getName(); - - CommandContribution getCommandContribution(); -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPlugin.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPlugin.kt new file mode 100644 index 00000000..a496b4e2 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPlugin.kt @@ -0,0 +1,7 @@ +package org.opencds.cqf.cql.ls.server.plugin + +interface CqlLanguageServerPlugin { + fun getName(): String + + fun getCommandContribution(): CommandContribution? +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPluginFactory.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPluginFactory.java deleted file mode 100644 index 8f5d2d22..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPluginFactory.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.opencds.cqf.cql.ls.server.plugin; - -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.eclipse.lsp4j.services.WorkspaceService; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; - -public interface CqlLanguageServerPluginFactory { - CqlLanguageServerPlugin createPlugin( - CompletableFuture client, - WorkspaceService workspaceService, - TextDocumentService textDocumentService, - CqlCompilationManager compilationManager); -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPluginFactory.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPluginFactory.kt new file mode 100644 index 00000000..0d3e873d --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/plugin/CqlLanguageServerPluginFactory.kt @@ -0,0 +1,16 @@ +package org.opencds.cqf.cql.ls.server.plugin + +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.services.TextDocumentService +import org.eclipse.lsp4j.services.WorkspaceService +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import java.util.concurrent.CompletableFuture + +interface CqlLanguageServerPluginFactory { + fun createPlugin( + client: CompletableFuture, + workspaceService: WorkspaceService, + textDocumentService: TextDocumentService, + compilationManager: CqlCompilationManager + ): CqlLanguageServerPlugin +} 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 deleted file mode 100644 index 99b08449..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProvider.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.opencds.cqf.cql.ls.server.provider; - -import java.io.*; -import java.net.URI; -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.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; - -public class ContentServiceModelInfoProvider implements ModelInfoProvider { - - private static final Logger log = LoggerFactory.getLogger(ContentServiceModelInfoProvider.class); - - private final ContentService contentService; - private final URI root; - - public ContentServiceModelInfoProvider(URI root, ContentService contentService) { - this.contentService = contentService; - this.root = root; - } - - public ModelInfo load(ModelIdentifier modelIdentifier) { - if (root != null && contentService != null) { - String modelName = modelIdentifier.getId(); - String modelVersion = modelIdentifier.getVersion(); - - try { - URI modelUri = Uris.addPath( - root, - String.format( - "/%s-modelinfo%s.xml", - modelName.toLowerCase(), modelVersion != null ? ("-" + modelVersion) : "")); - InputStream modelInputStream = contentService.read(modelUri); - if (modelInputStream != null) { - return XmlModelInfoReaderKt.parseModelInfoXml(Converters.inputStreamToString(modelInputStream)); - } - } catch (Exception e) { - throw new IllegalArgumentException( - String.format("Could not load definition for model info %s.", modelIdentifier.getId()), e); - } - } - - return null; - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProvider.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProvider.kt new file mode 100644 index 00000000..e85d8f8d --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProvider.kt @@ -0,0 +1,37 @@ +package org.opencds.cqf.cql.ls.server.provider + +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.parseModelInfoXml +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.LoggerFactory +import java.net.URI + +class ContentServiceModelInfoProvider( + private val root: URI, + private val contentService: ContentService +) : ModelInfoProvider { + + companion object { + private val log = LoggerFactory.getLogger(ContentServiceModelInfoProvider::class.java) + } + + override fun load(modelIdentifier: ModelIdentifier): ModelInfo? { + val modelName = modelIdentifier.id + val modelVersion = modelIdentifier.version + + return try { + val modelUri = Uris.addPath( + root, + "/${modelName.lowercase()}-modelinfo${if (modelVersion != null) "-$modelVersion" else ""}.xml" + ) ?: return null + val modelInputStream = contentService.read(modelUri) ?: return null + parseModelInfoXml(Converters.inputStreamToString(modelInputStream)) + } catch (e: Exception) { + throw IllegalArgumentException("Could not load definition for model info ${modelIdentifier.id}.", e) + } + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.java deleted file mode 100644 index f716130f..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.opencds.cqf.cql.ls.server.provider; - -import static org.opencds.cqf.cql.ls.core.utility.Converters.inputStreamToSource; - -import java.io.IOException; -import java.io.InputStream; -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; - -public class ContentServiceSourceProvider implements LibrarySourceProvider { - - private final ContentService contentService; - private final URI root; - - public ContentServiceSourceProvider(URI root, ContentService contentService) { - this.contentService = contentService; - this.root = root; - } - - public Source getLibrarySource(VersionedIdentifier libraryIdentifier) { - try { - InputStream is = this.contentService.read(this.root, libraryIdentifier); - if (is != null) { - return inputStreamToSource(is); - } - return null; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.kt new file mode 100644 index 00000000..8e2505f5 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProvider.kt @@ -0,0 +1,19 @@ +package org.opencds.cqf.cql.ls.server.provider + +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 org.opencds.cqf.cql.ls.core.utility.Converters +import java.net.URI + +class ContentServiceSourceProvider( + private val root: URI, + private val contentService: ContentService +) : LibrarySourceProvider { + + override fun getLibrarySource(libraryIdentifier: VersionedIdentifier): Source? { + val inputStream = contentService.read(root, libraryIdentifier) ?: return null + return Converters.inputStreamToSource(inputStream) + } +} 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 deleted file mode 100644 index 00466f56..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/FormattingProvider.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.opencds.cqf.cql.ls.server.provider; - -import java.io.IOException; -import java.net.URI; -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.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; - -public class FormattingProvider { - 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; - 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); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/FormattingProvider.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/FormattingProvider.kt new file mode 100644 index 00000000..7f14e653 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/FormattingProvider.kt @@ -0,0 +1,36 @@ +package org.opencds.cqf.cql.ls.server.provider + +import org.cqframework.cql.tools.formatter.CqlFormatterVisitor +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 + +class FormattingProvider(private val contentService: ContentService) { + + fun format(uri: String): List { + val u = requireNotNull(Uris.parseOrNull(uri)) + + val fr = try { + CqlFormatterVisitor.Companion.getFormattedOutput( + contentService.read(u) ?: throw IllegalArgumentException("Unable to read content from: $u") + ) + } catch (e: Exception) { + throw IllegalArgumentException("Unable to format CQL due to an error.", e) + } + + if (fr.errors.isNotEmpty()) { + throw IllegalArgumentException( + listOf("Unable to format CQL due to syntax errors.", "Please fix the errors and try again.").joinToString("\n") + ) + } + + val te = TextEdit( + Range(Position(0, 0), Position(Int.MAX_VALUE, Int.MAX_VALUE)), + fr.output + ) + + return listOf(te) + } +} 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 deleted file mode 100644 index 58320c30..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/HoverProvider.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.opencds.cqf.cql.ls.server.provider; - -import java.net.URI; -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; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; - -public class HoverProvider { - private final CqlCompilationManager cqlCompilationManager; - - public HoverProvider(CqlCompilationManager cqlCompilationManager) { - this.cqlCompilationManager = cqlCompilationManager; - } - - public Hover hover(HoverParams position) { - URI uri = Uris.parseOrNull(position.getTextDocument().getUri()); - if (uri == null) { - return null; - } - - // This translates on the fly. We may want to consider maintaining - // an ELM index to reduce the need to do retranslation. - CqlCompiler compiler = this.cqlCompilationManager.compile(uri); - if (compiler == null) { - return null; - } - - // The ExpressionTrackBackVisitor is supposed to replace this eventually. - // Basically, for any given position in the text document there's a graph of nodes - // that represent the parents nodes for that position. For example: - // - // define: "EncounterExists": - // exists([Encounter]) - // - // ExpressionDef -> Expression -> Exists -> Retrieve - // - // 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) - // - // The current code always picks the first ExpressionDef in the graph. - Pair exp = getExpressionDefForPosition( - position.getPosition(), - compiler.getCompiledLibrary().getLibrary().getStatements()); - - if (exp == null) { - return null; - } - - MarkupContent markup = markup(exp.getRight()); - if (markup == null) { - return null; - } - - return new Hover(markup, exp.getLeft()); - } - - private Pair getExpressionDefForPosition(Position position, Statements statements) { - if (statements == null || statements.getDef().isEmpty()) { - return null; - } - 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; - } - - private boolean positionInTrackBack(Position p, TrackBack tb) { - int startLine = tb.getStartLine() - 1; - int endLine = tb.getEndLine() - 1; - - // Just kidding. We need intervals. - if (p.getLine() >= startLine && p.getLine() <= endLine) { - return true; - } else { - return false; - } - } - - public MarkupContent markup(ExpressionDef def) { - if (def == null || def.getExpression() == null) { - return null; - } - - DataType resultType = Trackable.INSTANCE.getResultType(def); - 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); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/HoverProvider.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/HoverProvider.kt new file mode 100644 index 00000000..629e67e2 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/provider/HoverProvider.kt @@ -0,0 +1,94 @@ +package org.opencds.cqf.cql.ls.server.provider + +import org.cqframework.cql.cql2elm.tracking.TrackBack +import org.cqframework.cql.cql2elm.tracking.Trackable.resultType +import org.cqframework.cql.cql2elm.tracking.Trackable.trackbacks +import org.eclipse.lsp4j.Hover +import org.eclipse.lsp4j.HoverParams +import org.eclipse.lsp4j.MarkupContent +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +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 + +class HoverProvider(private val cqlCompilationManager: CqlCompilationManager) { + + fun hover(position: HoverParams): Hover? { + val uri = Uris.parseOrNull(position.textDocument.uri) ?: return null + + // This translates on the fly. We may want to consider maintaining + // an ELM index to reduce the need to do retranslation. + val compiler = cqlCompilationManager.compile(uri) ?: return null + + // The ExpressionTrackBackVisitor is supposed to replace this eventually. + // Basically, for any given position in the text document there's a graph of nodes + // that represent the parents nodes for that position. For example: + // + // define: "EncounterExists": + // exists([Encounter]) + // + // ExpressionDef -> Expression -> Exists -> Retrieve + // + // 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) + // + // The current code always picks the first ExpressionDef in the graph. + val exp = getExpressionDefForPosition( + position.position, + compiler.compiledLibrary?.library?.statements + ) ?: return null + + val markup = markup(exp.second) ?: return null + + return Hover(markup, exp.first) + } + + private fun getExpressionDefForPosition(position: Position, statements: Statements?): Pair? { + if (statements == null || statements.def.isEmpty()) { + return null + } + for (def in statements.def) { + if (def.trackbacks.isEmpty()) { + continue + } + + for (tb in def.trackbacks) { + if (positionInTrackBack(position, tb)) { + val range = Range( + Position(tb.startLine - 1, tb.startChar - 1), + Position(tb.endLine - 1, tb.endChar) + ) + return Pair(range, def) + } + } + } + + return null + } + + private fun positionInTrackBack(p: Position, tb: TrackBack): Boolean { + val startLine = tb.startLine - 1 + val endLine = tb.endLine - 1 + + // Just kidding. We need intervals. + return p.line in startLine..endLine + } + + fun markup(def: ExpressionDef?): MarkupContent? { + if (def == null || def.expression == null) { + return null + } + + val resultType = def.resultType ?: return null + + // Specifying the Markdown type as cql allows the client to apply + // cql syntax highlighting the resulting pop-up + val result = listOf("```cql", resultType.toString(), "```").joinToString("\n") + + return MarkupContent("markdown", result) + } +} 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 deleted file mode 100644 index 7d46c7e7..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardConventions.java +++ /dev/null @@ -1,251 +0,0 @@ -package org.opencds.cqf.cql.ls.server.repository.ig.standard; - -import jakarta.annotation.Nonnull; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.hl7.fhir.r4.model.Enumerations.FHIRAllTypes; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This class represents the different file structures for an IG repository. The main differences - * between the various configurations are whether the files are organized by resource type - * and/or category, and whether the files are prefixed with the resource type. - */ -public record IgStandardConventions( - FhirTypeLayout typeLayout, - CategoryLayout categoryLayout, - CompartmentLayout compartmentLayout, - FilenameMode filenameMode) { - - private static final Logger logger = LoggerFactory.getLogger(IgStandardConventions.class); - - public enum FhirTypeLayout { - DIRECTORY_PER_TYPE, - FLAT - } - - public enum CategoryLayout { - DIRECTORY_PER_CATEGORY, - FLAT - } - - public enum CompartmentLayout { - DIRECTORY_PER_COMPARTMENT, - FLAT - } - - public enum FilenameMode { - TYPE_AND_ID, - ID_ONLY - } - - public static final IgStandardConventions FLAT = new IgStandardConventions( - FhirTypeLayout.FLAT, CategoryLayout.FLAT, CompartmentLayout.FLAT, FilenameMode.TYPE_AND_ID); - - public static final IgStandardConventions STANDARD = new IgStandardConventions( - FhirTypeLayout.DIRECTORY_PER_TYPE, - CategoryLayout.DIRECTORY_PER_CATEGORY, - CompartmentLayout.FLAT, - FilenameMode.ID_ONLY); - - private static final List FHIR_TYPE_NAMES = Stream.of(FHIRAllTypes.values()) - .map(FHIRAllTypes::name) - .map(String::toLowerCase) - .distinct() - .toList(); - - /** - * Auto-detect the IG conventions based on the structure of the IG. If the path is null or the - * convention can not be reliably detected, the default configuration is returned. - * - * @param path The path to the IG. - * @return The IG conventions. - */ - public static IgStandardConventions autoDetect(Path path) { - - if (path == null || !Files.exists(path)) { - return STANDARD; - } - - // A "category" hierarchy may exist in the ig file structure, - // where resource categories ("data", "terminology", "content") are organized into - // subdirectories ("tests", "vocabulary", "resources"). - // - // e.g. "input/tests", "input/vocabulary". - // - // Check all possible category paths and grab the first that exists, - // or use the IG path if none exist. - - var categoryPath = Stream.of("tests", "vocabulary", "resources") - .map(path::resolve) - .filter(x -> x.toFile().exists()) - .findFirst() - .orElse(path); - - var hasCategoryDirectory = !path.equals(categoryPath); - - var hasCompartmentDirectory = false; - - // Compartments can only exist for test data - if (hasCategoryDirectory) { - var tests = path.resolve("tests"); - // A compartment under the tests looks like a set of subdirectories - // e.g. "input/tests/Patient", "input/tests/Practitioner" - // that themselves contain subdirectories for each test case. - // e.g. "input/tests/Patient/test1", "input/tests/Patient/test2" - // Then within those, the structure may be flat (e.g. "input/tests/Patient/test1/123.json") - // or grouped by type (e.g. "input/tests/Patient/test1/Patient/123.json"). - // - // The trick is that the in the case that the test cases are - // grouped by type, the compartment directory will be the same as the type directory. - // so we need to look at the resource type directory and check if the contents are files - // or more directories. If more directories exist, and the directory name is not a - // FHIR type, then we have a compartment directory. - if (tests.toFile().exists()) { - var compartments = FHIR_TYPE_NAMES.stream().map(tests::resolve).filter(x -> x.toFile() - .exists()); - - final List compartmentsList = compartments.toList(); - - // Check if any of the potential compartment directories - // have subdirectories that are not FHIR types (e.g. "input/tests/Patient/test1). - hasCompartmentDirectory = compartmentsList.stream() - .flatMap(IgStandardConventions::listFiles) - .filter(Files::isDirectory) - .anyMatch(IgStandardConventions::matchesAnyResource); - } - } - - // A "type" may also exist in the igs file structure, where resources - // are grouped by type into subdirectories. - // - // e.g. "input/vocabulary/valueset", "input/resources/valueset". - // - // Check all possible type paths and grab the first that exists, - // or use the category directory if none exist - var typePath = FHIR_TYPE_NAMES.stream() - .map(categoryPath::resolve) - .filter(Files::exists) - .findFirst() - .orElse(categoryPath); - - var hasTypeDirectory = !categoryPath.equals(typePath); - - // A file "claims" to be a FHIR resource type if its filename starts with a valid FHIR type name. - // For files that "claim" to be a FHIR resource type, we check to see if the contents of the file - // have a resource that matches the claimed type. - var hasTypeFilename = hasTypeFilename(typePath); - - var config = new IgStandardConventions( - hasTypeDirectory ? FhirTypeLayout.DIRECTORY_PER_TYPE : FhirTypeLayout.FLAT, - hasCategoryDirectory ? CategoryLayout.DIRECTORY_PER_CATEGORY : CategoryLayout.FLAT, - hasCompartmentDirectory ? CompartmentLayout.DIRECTORY_PER_COMPARTMENT : CompartmentLayout.FLAT, - hasTypeFilename ? FilenameMode.TYPE_AND_ID : FilenameMode.ID_ONLY); - - logger.info("Auto-detected repository configuration: {}", config); - - return config; - } - - private static boolean hasTypeFilename(Path typePath) { - try (var fileStream = Files.list(typePath)) { - return fileStream - .filter(IgStandardConventions::fileNameMatchesType) - .filter(filePath -> claimedFhirType(filePath) != FHIRAllTypes.NULL) - .anyMatch(filePath -> contentsMatchClaimedType(filePath, claimedFhirType(filePath))); - } catch (IOException exception) { - logger.error("Error listing files in path: {}", typePath, exception); - return false; - } - } - - private static boolean fileNameMatchesType(Path innerFile) { - Objects.requireNonNull(innerFile); - var fileName = innerFile.getFileName().toString(); - return FHIR_TYPE_NAMES.stream().anyMatch(type -> fileName.toLowerCase().startsWith(type)); - } - - private static boolean matchesAnyResource(Path innerFile) { - return !FHIR_TYPE_NAMES.contains(innerFile.getFileName().toString().toLowerCase()); - } - - @Nonnull - private static Stream listFiles(Path innerPath) { - try { - return Files.list(innerPath); - } catch (IOException e) { - logger.error("Error listing files in path: {}", innerPath, e); - return Stream.empty(); - } - } - - // This method checks to see if the contents of a file match the type claimed by the filename - private static boolean contentsMatchClaimedType(Path filePath, FHIRAllTypes claimedFhirType) { - Objects.requireNonNull(filePath); - Objects.requireNonNull(claimedFhirType); - - try (var linesStream = Files.lines(filePath, StandardCharsets.UTF_8)) { - var contents = linesStream.collect(Collectors.joining()); - if (contents.isEmpty()) { - return false; - } - - var filename = filePath.getFileName().toString(); - var fileNameWithoutExtension = filename.substring(0, filename.lastIndexOf(".")); - // Check that the contents contain the claimed type, and that the id is not the same as the filename - // NOTE: This does not work for XML files. - return contents.toUpperCase().contains("\"RESOURCETYPE\": \"%s\"".formatted(claimedFhirType.name())) - && !contents.toUpperCase() - .contains("\"ID\": \"%s\"".formatted(fileNameWithoutExtension.toUpperCase())); - - } catch (IOException e) { - return false; - } - } - - // Detects the FHIR type claimed by the filename - private static FHIRAllTypes claimedFhirType(Path filePath) { - var filename = filePath.getFileName().toString(); - if (!filename.contains("-")) { - return FHIRAllTypes.NULL; - } - - var codeName = filename.substring(0, filename.indexOf("-")).toUpperCase(); - try { - return FHIRAllTypes.valueOf(codeName); - } catch (Exception e) { - return FHIRAllTypes.NULL; - } - } - - @Override - public boolean equals(Object other) { - if (other == null || getClass() != other.getClass()) { - return false; - } - IgStandardConventions that = (IgStandardConventions) other; - return typeLayout == that.typeLayout - && filenameMode == that.filenameMode - && categoryLayout == that.categoryLayout - && compartmentLayout == that.compartmentLayout; - } - - @Override - public int hashCode() { - return Objects.hash(typeLayout, categoryLayout, compartmentLayout, filenameMode); - } - - @Override - @Nonnull - public String toString() { - return "IGConventions [typeLayout=%s, categoryLayout=%s compartmentLayout=%s, filenameMode=%s]" - .formatted(typeLayout, categoryLayout, compartmentLayout, filenameMode); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardConventions.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardConventions.kt new file mode 100644 index 00000000..68dcc4ea --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardConventions.kt @@ -0,0 +1,176 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard + +import org.hl7.fhir.r4.model.Enumerations.FHIRAllTypes +import org.slf4j.LoggerFactory +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.util.stream.Stream + +/** + * This class represents the different file structures for an IG repository. The main differences + * between the various configurations are whether the files are organized by resource type + * and/or category, and whether the files are prefixed with the resource type. + */ +data class IgStandardConventions( + @get:JvmName("typeLayout") val typeLayout: FhirTypeLayout, + @get:JvmName("categoryLayout") val categoryLayout: CategoryLayout, + @get:JvmName("compartmentLayout") val compartmentLayout: CompartmentLayout, + @get:JvmName("filenameMode") val filenameMode: FilenameMode +) { + + enum class FhirTypeLayout { + DIRECTORY_PER_TYPE, + FLAT + } + + enum class CategoryLayout { + DIRECTORY_PER_CATEGORY, + FLAT + } + + enum class CompartmentLayout { + DIRECTORY_PER_COMPARTMENT, + FLAT + } + + enum class FilenameMode { + TYPE_AND_ID, + ID_ONLY + } + + override fun toString(): String = + "IGConventions [typeLayout=$typeLayout, categoryLayout=$categoryLayout compartmentLayout=$compartmentLayout, filenameMode=$filenameMode]" + + companion object { + private val logger = LoggerFactory.getLogger(IgStandardConventions::class.java) + + @JvmField + val FLAT = IgStandardConventions( + FhirTypeLayout.FLAT, CategoryLayout.FLAT, CompartmentLayout.FLAT, FilenameMode.TYPE_AND_ID + ) + + @JvmField + val STANDARD = IgStandardConventions( + FhirTypeLayout.DIRECTORY_PER_TYPE, + CategoryLayout.DIRECTORY_PER_CATEGORY, + CompartmentLayout.FLAT, + FilenameMode.ID_ONLY + ) + + private val FHIR_TYPE_NAMES: List = FHIRAllTypes.values() + .map { it.name.lowercase() } + .distinct() + + /** + * Auto-detect the IG conventions based on the structure of the IG. + */ + @JvmStatic + fun autoDetect(path: Path?): IgStandardConventions { + if (path == null || !Files.exists(path)) { + return STANDARD + } + + val categoryPath = listOf("tests", "vocabulary", "resources") + .map { path.resolve(it) } + .firstOrNull { it.toFile().exists() } + ?: path + + val hasCategoryDirectory = path != categoryPath + + var hasCompartmentDirectory = false + + if (hasCategoryDirectory) { + val tests = path.resolve("tests") + if (tests.toFile().exists()) { + val compartments = FHIR_TYPE_NAMES.map { tests.resolve(it) }.filter { it.toFile().exists() } + + hasCompartmentDirectory = compartments + .flatMap { listFiles(it).toList() } + .filter { Files.isDirectory(it) } + .any { matchesAnyResource(it) } + } + } + + val typePath = FHIR_TYPE_NAMES + .map { categoryPath.resolve(it) } + .firstOrNull { Files.exists(it) } + ?: categoryPath + + val hasTypeDirectory = categoryPath != typePath + val hasTypeFilename = hasTypeFilename(typePath) + + val config = IgStandardConventions( + if (hasTypeDirectory) FhirTypeLayout.DIRECTORY_PER_TYPE else FhirTypeLayout.FLAT, + if (hasCategoryDirectory) CategoryLayout.DIRECTORY_PER_CATEGORY else CategoryLayout.FLAT, + if (hasCompartmentDirectory) CompartmentLayout.DIRECTORY_PER_COMPARTMENT else CompartmentLayout.FLAT, + if (hasTypeFilename) FilenameMode.TYPE_AND_ID else FilenameMode.ID_ONLY + ) + + logger.info("Auto-detected repository configuration: {}", config) + + return config + } + + private fun hasTypeFilename(typePath: Path): Boolean { + return try { + Files.list(typePath).use { fileStream -> + fileStream + .filter { fileNameMatchesType(it) } + .filter { claimedFhirType(it) != FHIRAllTypes.NULL } + .anyMatch { contentsMatchClaimedType(it, claimedFhirType(it)) } + } + } catch (e: IOException) { + logger.error("Error listing files in path: {}", typePath, e) + false + } + } + + private fun fileNameMatchesType(innerFile: Path): Boolean { + val fileName = innerFile.fileName.toString() + return FHIR_TYPE_NAMES.any { type -> fileName.lowercase().startsWith(type) } + } + + private fun matchesAnyResource(innerFile: Path): Boolean { + return !FHIR_TYPE_NAMES.contains(innerFile.fileName.toString().lowercase()) + } + + private fun listFiles(innerPath: Path): Stream { + return try { + Files.list(innerPath) + } catch (e: IOException) { + logger.error("Error listing files in path: {}", innerPath, e) + Stream.empty() + } + } + + private fun contentsMatchClaimedType(filePath: Path, claimedFhirType: FHIRAllTypes): Boolean { + return try { + Files.lines(filePath, StandardCharsets.UTF_8).use { linesStream -> + val contents = linesStream.collect(java.util.stream.Collectors.joining()) + if (contents.isEmpty()) return false + + val filename = filePath.fileName.toString() + val fileNameWithoutExtension = filename.substring(0, filename.lastIndexOf(".")) + contents.uppercase().contains("\"RESOURCETYPE\": \"${claimedFhirType.name}\"") && + !contents.uppercase().contains("\"ID\": \"${fileNameWithoutExtension.uppercase()}\"") + } + } catch (e: IOException) { + false + } + } + + private fun claimedFhirType(filePath: Path): FHIRAllTypes { + val filename = filePath.fileName.toString() + if (!filename.contains("-")) return FHIRAllTypes.NULL + + val codeName = filename.substring(0, filename.indexOf("-")).uppercase() + return try { + FHIRAllTypes.valueOf(codeName) + } catch (e: Exception) { + FHIRAllTypes.NULL + } + } + } +} 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 deleted file mode 100644 index 13cd2d62..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardCqlContent.java +++ /dev/null @@ -1,73 +0,0 @@ -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/IgStandardCqlContent.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardCqlContent.kt new file mode 100644 index 00000000..d753eaa3 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardCqlContent.kt @@ -0,0 +1,59 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard + +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException +import org.hl7.fhir.instance.model.api.IBaseResource +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path + +object IgStandardCqlContent { + + @JvmStatic + fun loadCqlContent(resource: IBaseResource, resourcePath: Path) { + requireNotNull(resource) { "resource can not be null" } + requireNotNull(resourcePath) { "resourcePath can not be null" } + + if ("Library" != resource.fhirType()) { + return + } + + val (cqlPathExtractor, cqlContentAttacher) = when (resource.structureFhirVersionEnum) { + ca.uhn.fhir.context.FhirVersionEnum.DSTU3 -> Pair( + { r: IBaseResource -> org.opencds.cqf.fhir.utility.dstu3.AttachmentUtil.getCqlLocation(r) }, + { r: IBaseResource, s: String -> org.opencds.cqf.fhir.utility.dstu3.AttachmentUtil.addData(r, s); Unit } + ) + ca.uhn.fhir.context.FhirVersionEnum.R4 -> Pair( + { r: IBaseResource -> org.opencds.cqf.fhir.utility.r4.AttachmentUtil.getCqlLocation(r) }, + { r: IBaseResource, s: String -> org.opencds.cqf.fhir.utility.r4.AttachmentUtil.addData(r, s); Unit } + ) + ca.uhn.fhir.context.FhirVersionEnum.R5 -> Pair( + { r: IBaseResource -> org.opencds.cqf.fhir.utility.r5.AttachmentUtil.getCqlLocation(r) }, + { r: IBaseResource, s: String -> org.opencds.cqf.fhir.utility.r5.AttachmentUtil.addData(r, s); Unit } + ) + else -> throw IllegalArgumentException("Unsupported FHIR version: ${resource.structureFhirVersionEnum}") + } + + readAndAttachCqlContent(resource, resourcePath, cqlPathExtractor, cqlContentAttacher) + } + + private fun readAndAttachCqlContent( + resource: IBaseResource, + resourcePath: Path, + cqlPathExtractor: (IBaseResource) -> String?, + cqlContentAttacher: (IBaseResource, String) -> Unit + ) { + val cqlPath = cqlPathExtractor(resource) ?: return + val cqlContent = getCqlContent(resourcePath, cqlPath) + cqlContentAttacher(resource, cqlContent) + } + + internal fun getCqlContent(rootPath: Path, relativePath: String): String { + val path = rootPath.resolve(relativePath).normalize() + return try { + String(Files.readAllBytes(path), StandardCharsets.UTF_8) + } catch (e: IOException) { + throw ResourceNotFoundException("Unable to read CQL content from path: $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 deleted file mode 100644 index 9c402f73..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardEncodingBehavior.java +++ /dev/null @@ -1,41 +0,0 @@ -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/IgStandardEncodingBehavior.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardEncodingBehavior.kt new file mode 100644 index 00000000..89ca5257 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardEncodingBehavior.kt @@ -0,0 +1,31 @@ +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. + */ +class IgStandardEncodingBehavior( + internal val preferredEncoding: EncodingEnum, + internal val preserveEncoding: PreserveEncoding +) { + + /** + * 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. + */ + enum class PreserveEncoding { + PRESERVE_ORIGINAL_ENCODING, + OVERWRITE_WITH_PREFERRED_ENCODING + } + + companion object { + @JvmField + val DEFAULT = IgStandardEncodingBehavior( + EncodingEnum.JSON, PreserveEncoding.PRESERVE_ORIGINAL_ENCODING + ) + } +} 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 deleted file mode 100644 index ef1406b8..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.java +++ /dev/null @@ -1,906 +0,0 @@ -package org.opencds.cqf.cql.ls.server.repository.ig.standard; - -import static java.util.Objects.requireNonNull; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.repository.IRepository; -import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.param.TokenParam; -import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; -import ca.uhn.fhir.util.BundleBuilder; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.collect.BiMap; -import com.google.common.collect.ImmutableBiMap; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Multimap; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Predicate; -import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.opencds.cqf.fhir.utility.Ids; -import org.opencds.cqf.fhir.utility.matcher.ResourceMatcher; -import org.opencds.cqf.fhir.utility.repository.IRepositoryOperationProvider; -import org.opencds.cqf.fhir.utility.repository.Repositories; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Provides access to FHIR resources stored in a directory structure following - * Implementation Guide (IG) conventions. - * Supports CRUD operations and resource management based on IG directory and - * file naming conventions. - * - *

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

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

- * Key Features: - *

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

- * Example (based on conventions): - *

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

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

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

- * Example Usage: - *

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

- * Example Usage: - *

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

- * Example Usage: - *

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

- * Example Usage: - *

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

- * Example Usage: - *

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

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

The type of the parameters for the operation. - * @param The type of the resource identifier. - * @param id The identifier of the resource. - * @param name The name of the operation. - * @param parameters The operation parameters. - * @param returnType The expected return type. - * @param headers Additional headers (not used in this implementation). - * @return The result of the operation. - */ - @Override - public R invoke( - I id, String name, P parameters, Class returnType, Map headers) { - return invokeOperation(id, id.getResourceType(), name, parameters); - } - - protected R invokeOperation( - IIdType id, String resourceType, String operationName, IBaseParameters parameters) { - if (operationProvider == null) { - throw new IllegalArgumentException("No operation provider found. Unable to invoke operations."); - } - return operationProvider.invokeOperation(this, id, resourceType, operationName, parameters); - } - - protected IgStandardRepositoryCompartment compartmentFrom(Map headers) { - if (headers == null) { - return new IgStandardRepositoryCompartment(); - } - - var compartmentHeader = headers.get(FHIR_COMPARTMENT_HEADER); - return compartmentHeader == null - ? new IgStandardRepositoryCompartment() - : new IgStandardRepositoryCompartment(compartmentHeader); - } - - protected String pathForCompartment(IgStandardRepositoryCompartment igStandardRepositoryCompartment) { - if (igStandardRepositoryCompartment.isEmpty()) { - return ""; - } - // The compartment path is typically ResourceType/Id (e.g., Patient/123) - // This is used as a directory name. - return igStandardRepositoryCompartment.getType() + "/" + igStandardRepositoryCompartment.getId(); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt new file mode 100644 index 00000000..d603ab30 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt @@ -0,0 +1,513 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard + +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.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 org.hl7.fhir.instance.model.api.IBaseBundle +import org.hl7.fhir.instance.model.api.IBaseParameters +import org.hl7.fhir.instance.model.api.IBaseResource +import org.hl7.fhir.instance.model.api.IIdType +import org.opencds.cqf.fhir.utility.Ids +import org.opencds.cqf.fhir.utility.matcher.ResourceMatcher +import org.opencds.cqf.fhir.utility.repository.IRepositoryOperationProvider +import org.opencds.cqf.fhir.utility.repository.Repositories +import org.slf4j.LoggerFactory +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.concurrent.ConcurrentHashMap + +open class IgStandardRepository : IRepository { + + companion object { + private val log = LoggerFactory.getLogger(IgStandardRepository::class.java) + + const val SOURCE_PATH_TAG = "sourcePath" + const val EXTERNAL_DIRECTORY = "external" + const val FHIR_COMPARTMENT_HEADER = "X-FHIR-Compartment" + + @JvmField + val CATEGORY_DIRECTORIES: Map = ImmutableMap.builder() + .put(IgStandardResourceCategory.CONTENT, "resources") + .put(IgStandardResourceCategory.DATA, "tests") + .put(IgStandardResourceCategory.TERMINOLOGY, "vocabulary") + .build() + + @JvmField + val FILE_EXTENSIONS: BiMap = ImmutableBiMap.builder() + .put(EncodingEnum.JSON, "json") + .put(EncodingEnum.XML, "xml") + .put(EncodingEnum.RDF, "rdf") + .build() + + private fun parserForEncoding(fhirContext: FhirContext, encodingEnum: EncodingEnum): IParser { + return when (encodingEnum) { + EncodingEnum.JSON -> fhirContext.newJsonParser() + EncodingEnum.XML -> fhirContext.newXmlParser() + EncodingEnum.RDF -> fhirContext.newRDFParser() + else -> throw IllegalArgumentException("NDJSON is not supported") + } + } + } + + private val fhirContext: FhirContext + private val root: Path + private val conventions: IgStandardConventions + private val encodingBehavior: IgStandardEncodingBehavior + private val resourceMatcher: ResourceMatcher + private var operationProvider: IRepositoryOperationProvider? + + private val resourceCache = CacheBuilder.newBuilder() + .concurrencyLevel(10) + .maximumSize(500) + .build() + + /** + * Creates a new IgRepository with auto-detected conventions and default encoding behavior. + */ + constructor(fhirContext: FhirContext, root: Path) : + this(fhirContext, root, IgStandardConventions.autoDetect(root), IgStandardEncodingBehavior.DEFAULT, null) + + constructor( + fhirContext: FhirContext, + root: Path, + conventions: IgStandardConventions, + encodingBehavior: IgStandardEncodingBehavior, + operationProvider: IRepositoryOperationProvider? + ) { + this.fhirContext = requireNotNull(fhirContext) { "fhirContext cannot be null" } + this.root = requireNotNull(root) { "root cannot be null" } + this.conventions = requireNotNull(conventions) { "conventions is required" } + this.encodingBehavior = requireNotNull(encodingBehavior) { "encodingBehavior is required" } + this.resourceMatcher = Repositories.getResourceMatcher(this.fhirContext) + this.operationProvider = operationProvider + } + + fun setOperationProvider(operationProvider: IRepositoryOperationProvider) { + this.operationProvider = operationProvider + } + + fun clearCache() { + resourceCache.invalidateAll() + } + + private fun isExternalPath(path: Path): Boolean = + path.parent != null && path.parent.toString().lowercase().endsWith(EXTERNAL_DIRECTORY) + + protected open fun preferredPathForResource( + resourceType: Class, id: I, igRepositoryCompartment: IgStandardRepositoryCompartment + ): Path { + val directory = directoryForResource(resourceType, igRepositoryCompartment) + val fileName = fileNameForResource(resourceType.simpleName, id.idPart, encodingBehavior.preferredEncoding) + return directory.resolve(fileName) + } + + protected open fun potentialPathsForResource( + resourceType: Class, id: I, igRepositoryCompartment: IgStandardRepositoryCompartment + ): List { + val potentialDirectories = mutableListOf() + val directory = directoryForResource(resourceType, igRepositoryCompartment) + potentialDirectories.add(directory) + + if (IgStandardResourceCategory.forType(resourceType.simpleName) == IgStandardResourceCategory.TERMINOLOGY) { + potentialDirectories.add(directory.resolve(EXTERNAL_DIRECTORY)) + } + + val potentialPaths = mutableListOf() + for (dir in potentialDirectories) { + for (encoding in FILE_EXTENSIONS.keys) { + potentialPaths.add(dir.resolve(fileNameForResource(resourceType.simpleName, id.idPart, encoding))) + } + } + + return potentialPaths + } + + protected open fun fileNameForResource(resourceType: String, resourceId: String, encoding: EncodingEnum): String { + val name = "$resourceId.${FILE_EXTENSIONS[encoding]}" + return if (IgStandardConventions.FilenameMode.ID_ONLY == conventions.filenameMode) name + else "$resourceType-$name" + } + + protected open fun directoryForCategory( + resourceType: Class, igStandardRepositoryCompartment: IgStandardRepositoryCompartment + ): Path { + if (conventions.categoryLayout == IgStandardConventions.CategoryLayout.FLAT) { + return root + } + + val category = IgStandardResourceCategory.forType(resourceType.simpleName) + val directory = CATEGORY_DIRECTORIES[category]!! + val categoryPath = root.resolve(directory) + + if (conventions.compartmentLayout == IgStandardConventions.CompartmentLayout.DIRECTORY_PER_COMPARTMENT && + !igStandardRepositoryCompartment.isEmpty() + ) { + if (category == IgStandardResourceCategory.DATA) { + return categoryPath.resolve(pathForCompartment(igStandardRepositoryCompartment)) + } + } + + return categoryPath + } + + protected open fun directoryForResource( + resourceType: Class, igRepositoryCompartment: IgStandardRepositoryCompartment + ): Path { + val directory = directoryForCategory(resourceType, igRepositoryCompartment) + if (conventions.typeLayout == IgStandardConventions.FhirTypeLayout.FLAT) { + return directory + } + return directory.resolve(resourceType.simpleName.lowercase()) + } + + protected open fun readResource(path: Path): IBaseResource? { + log.info("IgStandardRepository.readResource - Attempting to read resource from path: {}", path) + val file = path.toFile() + if (!file.exists()) { + log.info("IgStandardRepository.readResource - Didn't find file") + return null + } + + val extension = fileExtension(path) ?: run { + log.info("IgStandardRepository.readResource - Extension check failed") + return null + } + + val encoding = FILE_EXTENSIONS.inverse()[extension] ?: return null + + return try { + val s = String(Files.readAllBytes(path), StandardCharsets.UTF_8) + val resource = parserForEncoding(fhirContext, encoding).parseResource(s) + resource.setUserData(SOURCE_PATH_TAG, path) + IgStandardCqlContent.loadCqlContent(resource, path.parent) + log.info("IgStandardRepository.readResource - Returning resource: {}", resource) + resource + } catch (e: FileNotFoundException) { + null + } catch (e: DataFormatException) { + throw ResourceNotFoundException("Found empty or invalid content at path $path") + } catch (e: IOException) { + throw UnclassifiedServerFailureException(500, "Unable to read resource from path $path") + } + } + + protected open fun cachedReadResource(path: Path): IBaseResource? { + val cached = resourceCache.getIfPresent(path) + if (cached != null) { + log.info("IgStandardRepository.cachedReadResource - Returning cached resource: {}", cached) + return cached + } + val resource = readResource(path) + if (resource != null) { + resourceCache.put(path, resource) + } + log.info("IgStandardRepository.cachedReadResource - Returning freshly loaded resource: {}", resource) + return resource + } + + protected open fun encodingForPath(path: Path): EncodingEnum? = FILE_EXTENSIONS.inverse()[fileExtension(path)] + + protected open fun writeResource(resource: T, path: Path) { + try { + path.parent?.toFile()?.mkdirs() + FileOutputStream(path.toFile()).use { stream -> + val result = parserForEncoding(fhirContext, encodingForPath(path)!!) + .setPrettyPrint(true) + .encodeResourceToString(resource) + stream.write(result.toByteArray()) + resource.setUserData(SOURCE_PATH_TAG, path) + resourceCache.put(path, resource) + } + } catch (e: Exception) { + when (e) { + is IOException, is SecurityException -> + throw UnclassifiedServerFailureException(500, "Unable to write resource to path $path") + else -> throw e + } + } + } + + private fun fileExtension(path: Path): String? { + val name = path.fileName.toString() + val lastPeriod = name.lastIndexOf(".") + if (lastPeriod == -1) return null + return name.substring(lastPeriod + 1).lowercase() + } + + private fun acceptByFileExtension(path: Path): Boolean { + val extension = fileExtension(path) ?: return false + return FILE_EXTENSIONS.containsValue(extension) + } + + private fun acceptByFileExtensionAndPrefix(path: Path, prefix: String): Boolean { + if (!acceptByFileExtension(path)) return false + return path.fileName.toString().lowercase().startsWith(prefix.lowercase() + "-") + } + + protected open fun readDirectoryForResourceType( + resourceClass: Class, igRepositoryCompartment: IgStandardRepositoryCompartment + ): Map { + val path = directoryForResource(resourceClass, igRepositoryCompartment) + if (!path.toFile().exists()) return emptyMap() + + val resources = ConcurrentHashMap() + val resourceFileFilter: (Path) -> Boolean = when (conventions.filenameMode) { + IgStandardConventions.FilenameMode.ID_ONLY -> ::acceptByFileExtension + else -> { p -> acceptByFileExtensionAndPrefix(p, resourceClass.simpleName) } + } + + try { + Files.walk(path).use { paths -> + paths.filter(resourceFileFilter) + .parallel() + .map { cachedReadResource(it) } + .filter { it != null } + .forEach { r -> + if (r!!.fhirType() != resourceClass.simpleName) return@forEach + val validated = validateResource(resourceClass, r, r.idElement) + resources[r.idElement.toUnqualifiedVersionless()] = validated + } + } + } catch (e: IOException) { + throw UnclassifiedServerFailureException(500, "Unable to read resources from path: $path") + } + + return resources + } + + override fun fhirContext(): FhirContext = fhirContext + + override fun read( + resourceType: Class, id: I, headers: Map? + ): T { + requireNotNull(resourceType) { "resourceType cannot be null" } + requireNotNull(id) { "id cannot be null" } + log.info("IgStandardRepository.read - Attempting to read resource [{}].", id) + log.info("IgStandardRepository.read - headers: {}", headers) + + val compartment = compartmentFrom(headers) + val paths = potentialPathsForResource(resourceType, id, compartment) + for (path in paths) { + log.info("IgStandardRepository.read - potentialPathsForResource path: {}", path) + if (!Files.exists(path)) { + log.info("IgStandardRepository.read - File doesn't exist at [{}]. Continuing loop.", path) + continue + } + + val 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 ResourceNotFoundException(id) + } + + override fun create(resource: T, headers: Map?): MethodOutcome { + requireNotNull(resource) { "resource cannot be null" } + requireNotNull(resource.idElement.idPart) { "resource id cannot be null" } + + val compartment = compartmentFrom(headers) + val path = preferredPathForResource(resource.javaClass, resource.idElement, compartment) + writeResource(resource, path) + + return MethodOutcome(resource.idElement, true) + } + + private fun validateResource(resourceType: Class, resource: IBaseResource, id: IIdType): T { + val path = resource.getUserData(SOURCE_PATH_TAG) as Path? + + if (resourceType.simpleName != resource.fhirType()) { + throw ResourceNotFoundException( + "Expected to find a resource with type: ${resourceType.simpleName} at path: $path. Found resource with type ${resource.fhirType()} instead." + ) + } + + if (!resource.idElement.hasIdPart()) { + throw ResourceNotFoundException( + "Expected to find a resource with id: ${id.toUnqualifiedVersionless()} at path: $path. Found resource without an id instead." + ) + } + + if (id.idPart != resource.idElement.idPart) { + throw ResourceNotFoundException( + "Expected to find a resource with id: ${id.idPart} at path: $path. Found resource with an id ${resource.idElement.idPart} instead." + ) + } + + if (id.hasVersionIdPart() && id.versionIdPart != resource.idElement.versionIdPart) { + throw ResourceNotFoundException( + "Expected to find a resource with version: ${id.versionIdPart} at path: $path. Found resource with version ${resource.idElement.versionIdPart} instead." + ) + } + + return resourceType.cast(resource) + } + + override fun update(resource: T, headers: Map?): MethodOutcome { + requireNotNull(resource) { "resource cannot be null" } + requireNotNull(resource.idElement.idPart) { "resource id cannot be null" } + + val compartment = compartmentFrom(headers) + val preferred = preferredPathForResource(resource.javaClass, resource.idElement, compartment) + var actual = resource.getUserData(SOURCE_PATH_TAG) as Path? ?: preferred + + if (isExternalPath(actual)) { + throw ForbiddenOperationException( + "Unable to create or update: ${resource.idElement.toUnqualifiedVersionless()}. Resource is marked as external, and external resources are read-only." + ) + } + + if (preferred != actual && + encodingBehavior.preserveEncoding == IgStandardEncodingBehavior.PreserveEncoding.OVERWRITE_WITH_PREFERRED_ENCODING + ) { + try { + Files.deleteIfExists(actual) + } catch (e: IOException) { + throw UnclassifiedServerFailureException(500, "Couldn't change encoding for $actual") + } + actual = preferred + } + + writeResource(resource, actual) + + return MethodOutcome(resource.idElement, false) + } + + override fun delete( + resourceType: Class, id: I, headers: Map? + ): MethodOutcome { + requireNotNull(resourceType) { "resourceType cannot be null" } + requireNotNull(id) { "id cannot be null" } + + val compartment = compartmentFrom(headers) + val paths = potentialPathsForResource(resourceType, id, compartment) + var deleted = false + for (path in paths) { + try { + deleted = Files.deleteIfExists(path) + if (deleted) break + } catch (e: IOException) { + throw UnclassifiedServerFailureException(500, "Couldn't delete $path") + } + } + + if (!deleted) throw ResourceNotFoundException(id) + + return MethodOutcome(id) + } + + @Suppress("UNCHECKED_CAST") + override fun search( + bundleType: Class, + resourceType: Class, + searchParameters: Multimap>, + headers: Map? + ): B { + val builder = BundleBuilder(fhirContext) + builder.setType("searchset") + + val compartment = compartmentFrom(headers) + val resourceIdMap = readDirectoryForResourceType(resourceType, compartment) + + if (searchParameters == null || searchParameters.isEmpty) { + resourceIdMap.values.forEach { builder.addCollectionEntry(it) } + return builder.bundle as B + } + + val candidates: Collection + if (searchParameters.containsKey("_id")) { + candidates = getIdCandidates(searchParameters["_id"], resourceIdMap, resourceType) + searchParameters.removeAll("_id") + } else { + candidates = resourceIdMap.values + } + + for (resource in candidates) { + if (allParametersMatch(searchParameters, resource)) { + builder.addCollectionEntry(resource) + } + } + + return builder.bundle as B + } + + private fun getIdCandidates( + idQueries: Collection>, resourceIdMap: Map, resourceType: Class + ): List { + val idResources = mutableListOf() + for (idQuery in idQueries) { + for (query in idQuery) { + if (query is TokenParam) { + val id = Ids.newId(fhirContext, resourceType.simpleName, query.value) + val resource = resourceIdMap[id] + if (resource != null) idResources.add(resource) + } + } + } + return idResources + } + + private fun allParametersMatch( + searchParameters: Multimap>, resource: IBaseResource + ): Boolean { + for (nextEntry in searchParameters.entries()) { + if (!resourceMatcher.matches(nextEntry.key, nextEntry.value, resource)) return false + } + return true + } + + override fun invoke( + resourceType: Class, name: String, parameters: P, returnType: Class, headers: Map? + ): R { + return invokeOperation(null, resourceType.simpleName, name, parameters) + } + + override fun invoke( + id: I, name: String, parameters: P, returnType: Class, headers: Map? + ): R { + return invokeOperation(id, id.resourceType, name, parameters) + } + + protected open fun invokeOperation( + id: IIdType?, resourceType: String, operationName: String, parameters: IBaseParameters + ): R { + checkNotNull(operationProvider) { "No operation provider found. Unable to invoke operations." } + @Suppress("UNCHECKED_CAST") + return operationProvider!!.invokeOperation, R>( + this, id, resourceType, operationName, parameters + ) + } + + protected open fun compartmentFrom(headers: Map?): IgStandardRepositoryCompartment { + if (headers == null) return IgStandardRepositoryCompartment() + val compartmentHeader = headers[FHIR_COMPARTMENT_HEADER] + return if (compartmentHeader == null) IgStandardRepositoryCompartment() + else IgStandardRepositoryCompartment(compartmentHeader) + } + + protected open fun pathForCompartment(igStandardRepositoryCompartment: IgStandardRepositoryCompartment): String { + if (igStandardRepositoryCompartment.isEmpty()) return "" + return "${igStandardRepositoryCompartment.type}/${igStandardRepositoryCompartment.id}" + } +} 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 deleted file mode 100644 index d7875f18..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.opencds.cqf.cql.ls.server.repository.ig.standard; - -import static java.util.Objects.requireNonNull; - -import java.util.Objects; -import java.util.StringJoiner; -import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; - -/** - * Class that represents the compartment context for a given request within {@link IgRepository} only. - */ -public class IgStandardRepositoryCompartment { - - private final String type; - private final String id; - - private static String typeOfContext(String context) { - return context.split("/")[0]; - } - - private static String idOfContext(String context) { - return context.split("/")[1]; - } - - // Empty context (i.e. no compartment context) - public IgStandardRepositoryCompartment() { - this.type = null; - this.id = null; - } - - // Context in the format ResourceType/Id - public IgStandardRepositoryCompartment(String context) { - this(typeOfContext(context), idOfContext(context)); - } - - // Context in the format type and id - public IgStandardRepositoryCompartment(String type, String id) { - // Make this lowercase so the path will resolve on Linux (FYI: macOS is case-insensitive) - this.type = requireNonNullOrEmpty("type", type).toLowerCase(); - this.id = requireNonNullOrEmpty("id", id); - } - - public String getType() { - return this.type; - } - - public String getId() { - return this.id; - } - - public boolean isEmpty() { - return this.type == null || this.id == null; - } - - private static String requireNonNullOrEmpty(String name, String value) { - requireNonNull(name, "name cannot be null"); - if (value == null || value.isEmpty()) { - throw new IllegalArgumentException(name + " cannot be null or empty"); - } - return value; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - IgStandardRepositoryCompartment that = (IgStandardRepositoryCompartment) o; - return Objects.equals(type, that.type) && Objects.equals(id, that.id); - } - - @Override - public int hashCode() { - return Objects.hash(type, id); - } - - @Override - public String toString() { - return new StringJoiner(", ", IgStandardRepositoryCompartment.class.getSimpleName() + "[", "]") - .add("type='" + type + "'") - .add("id='" + id + "'") - .toString(); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.kt new file mode 100644 index 00000000..1f720c89 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.kt @@ -0,0 +1,51 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard + +/** + * Class that represents the compartment context for a given request within IgRepository only. + */ +class IgStandardRepositoryCompartment { + + val type: String? + val id: String? + + // Empty context (i.e. no compartment context) + constructor() { + this.type = null + this.id = null + } + + // Context in the format ResourceType/Id + constructor(context: String) : this(typeOfContext(context), idOfContext(context)) + + // Context in the format type and id + constructor(type: String, id: String) { + // Make this lowercase so the path will resolve on Linux (FYI: macOS is case-insensitive) + this.type = requireNonNullOrEmpty("type", type).lowercase() + this.id = requireNonNullOrEmpty("id", id) + } + + fun isEmpty(): Boolean = type == null || id == null + + override fun equals(other: Any?): Boolean { + if (other == null || javaClass != other.javaClass) return false + other as IgStandardRepositoryCompartment + return type == other.type && id == other.id + } + + override fun hashCode(): Int = java.util.Objects.hash(type, id) + + override fun toString(): String = + "${IgStandardRepositoryCompartment::class.simpleName}[type='$type', id='$id']" + + companion object { + private fun typeOfContext(context: String): String = context.split("/")[0] + private fun idOfContext(context: String): String = context.split("/")[1] + + private fun requireNonNullOrEmpty(name: String, value: String?): String { + if (value.isNullOrEmpty()) { + throw IllegalArgumentException("$name cannot be null or empty") + } + return value + } + } +} 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 deleted file mode 100644 index f7439fe4..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.opencds.cqf.cql.ls.server.repository.ig.standard; - -import com.google.common.collect.Sets; -import java.util.Set; - -enum IgStandardResourceCategory { - DATA, - TERMINOLOGY, - CONTENT; - - private static final Set TERMINOLOGY_RESOURCES = Sets.newHashSet("ValueSet", "CodeSystem"); - private static final Set CONTENT_RESOURCES = Sets.newHashSet( - "Library", "Questionnaire", "Measure", "PlanDefinition", "StructureDefinition", "ActivityDefinition"); - - static IgStandardResourceCategory forType(String resourceType) { - if (TERMINOLOGY_RESOURCES.contains(resourceType)) { - return TERMINOLOGY; - } else if (CONTENT_RESOURCES.contains(resourceType)) { - return CONTENT; - } - - return DATA; - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.kt new file mode 100644 index 00000000..03271694 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardResourceCategory.kt @@ -0,0 +1,22 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard + +enum class IgStandardResourceCategory { + DATA, + TERMINOLOGY, + CONTENT; + + companion object { + private val TERMINOLOGY_RESOURCES = setOf("ValueSet", "CodeSystem") + private val CONTENT_RESOURCES = setOf( + "Library", "Questionnaire", "Measure", "PlanDefinition", "StructureDefinition", "ActivityDefinition" + ) + + fun forType(resourceType: String): IgStandardResourceCategory { + return when { + TERMINOLOGY_RESOURCES.contains(resourceType) -> TERMINOLOGY + CONTENT_RESOURCES.contains(resourceType) -> CONTENT + else -> DATA + } + } + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.java deleted file mode 100644 index 709241f7..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.io.StringWriter; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.TextDocumentContentChangeEvent; -import org.eclipse.lsp4j.TextDocumentIdentifier; -import org.eclipse.lsp4j.TextDocumentItem; -import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; -import org.greenrobot.eventbus.Subscribe; -import org.hl7.elm.r1.VersionedIdentifier; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.core.utility.Uris; -import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent; - -public class ActiveContentService implements ContentService { - public static class VersionedContent { - public final String content; - public final int version; - - public VersionedContent(String content, int version) { - this.content = content; - this.version = version; - } - } - - private final Map activeContent = new ConcurrentHashMap<>(); - - @Override - public Set locate(URI root, VersionedIdentifier libraryIdentifier) { - checkNotNull(root); - checkNotNull(libraryIdentifier); - - return searchActiveContent(root, libraryIdentifier); - } - - @Override - public InputStream read(URI root, VersionedIdentifier identifier) { - checkNotNull(root); - checkNotNull(identifier); - - Set uris = this.locate(root, identifier); - - checkState(uris.size() == 1, "Found more than one file for identifier: {}", identifier); - - return this.read(uris.iterator().next()); - } - - @Override - public InputStream read(URI uri) { - checkNotNull(uri); - - String content = this.activeContent.get(uri).content; - return new ByteArrayInputStream(content.getBytes()); - } - - @Subscribe(priority = 100) - public void didOpen(DidOpenTextDocumentEvent e) { - TextDocumentItem document = e.params().getTextDocument(); - URI uri = Uris.parseOrNull(document.getUri()); - - String encodedText = new String(document.getText().getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); - - activeContent.put(uri, new VersionedContent(encodedText, document.getVersion())); - } - - @Subscribe(priority = 100) - public void didClose(DidCloseTextDocumentEvent e) { - TextDocumentIdentifier document = e.params().getTextDocument(); - URI uri = Uris.parseOrNull(document.getUri()); - activeContent.remove(uri); - } - - @Subscribe(priority = 100) - public void didChange(DidChangeTextDocumentEvent e) throws IOException { - VersionedTextDocumentIdentifier document = e.params().getTextDocument(); - URI uri = Uris.parseOrNull(document.getUri()); - - VersionedContent existing = activeContent.get(uri); - String existingText = existing.content; - - if (document.getVersion() > existing.version) { - for (TextDocumentContentChangeEvent change : e.params().getContentChanges()) { - if (change.getRange() == null) { - String encodedText = - new String(change.getText().getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); - activeContent.put(uri, new VersionedContent(encodedText, document.getVersion())); - } else { - String newText = patch(existingText, change); - activeContent.put(uri, new VersionedContent(newText, document.getVersion())); - } - } - } - } - - // Break this out into its own thing for test purposes. - @SuppressWarnings("deprecation") - protected String patch(String sourceText, TextDocumentContentChangeEvent change) throws IOException { - Range range = change.getRange(); - BufferedReader reader = new BufferedReader(new StringReader(sourceText)); - StringWriter writer = new StringWriter(); - - // Skip unchanged lines - int line = 0; - - while (line < range.getStart().getLine()) { - writer.write(reader.readLine() + '\n'); - line++; - } - - // Skip unchanged chars - for (int character = 0; character < range.getStart().getCharacter(); character++) writer.write(reader.read()); - - // Write replacement text - String encodedText = new String(change.getText().getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); - writer.write(encodedText); - - // Skip replaced text - reader.skip(change.getRangeLength()); - - // Write remaining text - while (true) { - int next = reader.read(); - - if (next == -1) return writer.toString(); - else writer.write(next); - } - } - - protected Set searchActiveContent(URI root, VersionedIdentifier identifier) { - String id = identifier.getId(); - String version = identifier.getVersion(); - - String matchText = "(?s).*library\\s+" + id; - if (version != null) { - matchText += ("\\s+version\\s+'" + version + "'\\s+(?s).*"); - } else { - matchText += "'\\s+(?s).*"; - } - - Set uris = new HashSet<>(); - - for (Entry entry : this.activeContent.entrySet()) { - URI uri = entry.getKey(); - // Checks to see if the current entry is a child of the root URI. - if (root.relativize(uri).equals(uri)) { - continue; - } - - String content = entry.getValue().content; - // This will match if the content contains the library definition is present. - if (content.matches(matchText)) { - uris.add(entry.getKey()); - } - } - - return uris; - } - - public Set activeUris() { - return this.activeContent.keySet(); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt new file mode 100644 index 00000000..0005b797 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt @@ -0,0 +1,135 @@ +package org.opencds.cqf.cql.ls.server.service + +import com.google.common.base.Preconditions.checkNotNull +import com.google.common.base.Preconditions.checkState +import org.eclipse.lsp4j.TextDocumentContentChangeEvent +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.TextDocumentItem +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier +import org.greenrobot.eventbus.Subscribe +import org.hl7.elm.r1.VersionedIdentifier +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent +import java.io.BufferedReader +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.io.StringReader +import java.io.StringWriter +import java.net.URI +import java.nio.charset.StandardCharsets +import java.util.concurrent.ConcurrentHashMap + +class ActiveContentService : ContentService { + + data class VersionedContent(val content: String, val version: Int) + + private val activeContent = ConcurrentHashMap() + + override fun locate(root: URI, identifier: VersionedIdentifier): Set { + checkNotNull(root) + checkNotNull(identifier) + return searchActiveContent(root, identifier) + } + + override fun read(root: URI, identifier: VersionedIdentifier): InputStream? { + checkNotNull(root) + checkNotNull(identifier) + val uris = locate(root, identifier) + checkState(uris.size == 1, "Found more than one file for identifier: %s", identifier) + return read(uris.first()) + } + + override fun read(uri: URI): InputStream? { + checkNotNull(uri) + val content = activeContent[uri]?.content ?: return null + return ByteArrayInputStream(content.toByteArray()) + } + + @Subscribe(priority = 100) + fun didOpen(e: DidOpenTextDocumentEvent) { + val document: TextDocumentItem = e.params().textDocument + val uri = Uris.parseOrNull(document.uri) ?: return + val encodedText = String(document.text.toByteArray(StandardCharsets.UTF_8), StandardCharsets.UTF_8) + activeContent[uri] = VersionedContent(encodedText, document.version) + } + + @Subscribe(priority = 100) + fun didClose(e: DidCloseTextDocumentEvent) { + val document: TextDocumentIdentifier = e.params().textDocument + val uri = Uris.parseOrNull(document.uri) ?: return + activeContent.remove(uri) + } + + @Subscribe(priority = 100) + fun didChange(e: DidChangeTextDocumentEvent) { + val document: VersionedTextDocumentIdentifier = e.params().textDocument + val uri = Uris.parseOrNull(document.uri) ?: return + val existing = activeContent[uri] ?: return + val existingText = existing.content + + if (document.version > existing.version) { + for (change in e.params().contentChanges) { + if (change.range == null) { + val encodedText = String(change.text.toByteArray(StandardCharsets.UTF_8), StandardCharsets.UTF_8) + activeContent[uri] = VersionedContent(encodedText, document.version) + } else { + val newText = patch(existingText, change) + activeContent[uri] = VersionedContent(newText, document.version) + } + } + } + } + + @Suppress("DEPRECATION") + internal fun patch(sourceText: String, change: TextDocumentContentChangeEvent): String { + val range = change.range + val reader = BufferedReader(StringReader(sourceText)) + val writer = StringWriter() + + var line = 0 + while (line < range.start.line) { + writer.write(reader.readLine() + '\n') + line++ + } + + for (character in 0 until range.start.character) { + writer.write(reader.read()) + } + + val encodedText = String(change.text.toByteArray(StandardCharsets.UTF_8), StandardCharsets.UTF_8) + writer.write(encodedText) + + reader.skip(change.rangeLength.toLong()) + + while (true) { + val next = reader.read() + if (next == -1) return writer.toString() + else writer.write(next) + } + } + + internal fun searchActiveContent(root: URI, identifier: VersionedIdentifier): Set { + val id = identifier.id + val version = identifier.version + var matchText = "(?s).*library\\s+$id" + matchText += if (version != null) { + "\\s+version\\s+'$version'\\s+(?s).*" + } else { + "'\\s+(?s).*" + } + + val uris = mutableSetOf() + for ((uri, content) in activeContent.entries) { + if (root.relativize(uri) == uri) continue + if (content.content.matches(matchText.toRegex())) { + uris.add(uri) + } + } + return uris + } + + fun activeUris(): Set = activeContent.keys +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.java deleted file mode 100644 index 0ee0a2b9..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.DidChangeTextDocumentParams; -import org.eclipse.lsp4j.DidCloseTextDocumentParams; -import org.eclipse.lsp4j.DidOpenTextDocumentParams; -import org.eclipse.lsp4j.DidSaveTextDocumentParams; -import org.eclipse.lsp4j.DocumentFormattingParams; -import org.eclipse.lsp4j.Hover; -import org.eclipse.lsp4j.HoverParams; -import org.eclipse.lsp4j.InitializeParams; -import org.eclipse.lsp4j.MessageParams; -import org.eclipse.lsp4j.MessageType; -import org.eclipse.lsp4j.ServerCapabilities; -import org.eclipse.lsp4j.TextDocumentSyncKind; -import org.eclipse.lsp4j.TextEdit; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.greenrobot.eventbus.EventBus; -import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.event.DidSaveTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.provider.FormattingProvider; -import org.opencds.cqf.cql.ls.server.provider.HoverProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CqlTextDocumentService implements TextDocumentService { - private static final Logger log = LoggerFactory.getLogger(CqlTextDocumentService.class); - - private final CompletableFuture client; - private final FormattingProvider formattingProvider; - private final HoverProvider hoverProvider; - private final EventBus eventBus; - - public CqlTextDocumentService( - CompletableFuture client, - HoverProvider hoverProvider, - FormattingProvider formattingProvider, - EventBus eventBus) { - this.client = client; - this.formattingProvider = formattingProvider; - this.hoverProvider = hoverProvider; - this.eventBus = eventBus; - } - - @SuppressWarnings("java:S125") // Keeping the commented code for future reference - public void initialize(InitializeParams params, ServerCapabilities serverCapabilities) { - serverCapabilities.setTextDocumentSync(TextDocumentSyncKind.Full); - // c.setDefinitionProvider(true); - // c.setCompletionProvider(new CompletionOptions(true, ImmutableList.of("."))); - serverCapabilities.setDocumentFormattingProvider(true); - // serverCapabilities.setDocumentRangeFormattingProvider(false); - serverCapabilities.setHoverProvider(true); - // c.setReferencesProvider(true); - // c.setDocumentSymbolProvider(true); - // c.setCodeActionProvider(true); - // c.setSignatureHelpProvider(new SignatureHelpOptions(ImmutableList.of("(", - // ","))); - } - - @Override - public CompletableFuture hover(HoverParams position) { - return CompletableFuture.supplyAsync(() -> this.hoverProvider.hover(position)) - .exceptionally(this::notifyClient); - } - - @Override - public CompletableFuture> formatting(DocumentFormattingParams params) { - return CompletableFuture.>supplyAsync(() -> - this.formattingProvider.format(params.getTextDocument().getUri())) - .exceptionally(this::notifyClient); - } - - private T notifyClient(Throwable e) { - log.error("error", e); - this.client.join().showMessage(new MessageParams(MessageType.Error, e.getMessage())); - return null; - } - - @Override - public void didOpen(DidOpenTextDocumentParams params) { - eventBus.post(new DidOpenTextDocumentEvent(params)); - } - - @Override - public void didChange(DidChangeTextDocumentParams params) { - eventBus.post(new DidChangeTextDocumentEvent(params)); - } - - @Override - public void didClose(DidCloseTextDocumentParams params) { - eventBus.post(new DidCloseTextDocumentEvent(params)); - } - - @Override - public void didSave(DidSaveTextDocumentParams params) { - eventBus.post(new DidSaveTextDocumentEvent(params)); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt new file mode 100644 index 00000000..337853b2 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt @@ -0,0 +1,86 @@ +package org.opencds.cqf.cql.ls.server.service + +import org.eclipse.lsp4j.DidChangeTextDocumentParams +import org.eclipse.lsp4j.DidCloseTextDocumentParams +import org.eclipse.lsp4j.DidOpenTextDocumentParams +import org.eclipse.lsp4j.DidSaveTextDocumentParams +import org.eclipse.lsp4j.DocumentFormattingParams +import org.eclipse.lsp4j.Hover +import org.eclipse.lsp4j.HoverParams +import org.eclipse.lsp4j.InitializeParams +import org.eclipse.lsp4j.MessageParams +import org.eclipse.lsp4j.MessageType +import org.eclipse.lsp4j.ServerCapabilities +import org.eclipse.lsp4j.TextDocumentSyncKind +import org.eclipse.lsp4j.TextEdit +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.services.TextDocumentService +import org.greenrobot.eventbus.EventBus +import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidSaveTextDocumentEvent +import org.opencds.cqf.cql.ls.server.provider.FormattingProvider +import org.opencds.cqf.cql.ls.server.provider.HoverProvider +import org.slf4j.LoggerFactory +import java.util.concurrent.CompletableFuture + +class CqlTextDocumentService( + private val client: CompletableFuture, + private val hoverProvider: HoverProvider, + private val formattingProvider: FormattingProvider, + private val eventBus: EventBus +) : TextDocumentService { + + companion object { + private val log = LoggerFactory.getLogger(CqlTextDocumentService::class.java) + } + + @Suppress("java:S125") // Keeping the commented code for future reference + fun initialize(params: InitializeParams, serverCapabilities: ServerCapabilities) { + serverCapabilities.setTextDocumentSync(TextDocumentSyncKind.Full) + // c.setDefinitionProvider(true); + // c.setCompletionProvider(new CompletionOptions(true, ImmutableList.of("."))); + serverCapabilities.setDocumentFormattingProvider(true) + // serverCapabilities.setDocumentRangeFormattingProvider(false); + serverCapabilities.setHoverProvider(true) + // c.setReferencesProvider(true); + // c.setDocumentSymbolProvider(true); + // c.setCodeActionProvider(true); + // c.setSignatureHelpProvider(new SignatureHelpOptions(ImmutableList.of("(", ","))); + } + + @Suppress("UNCHECKED_CAST") + override fun hover(position: HoverParams): CompletableFuture { + return CompletableFuture.supplyAsync { hoverProvider.hover(position) } + .exceptionally { notifyClient(it) } as CompletableFuture + } + + override fun formatting(params: DocumentFormattingParams): CompletableFuture> { + return CompletableFuture.supplyAsync> { + formattingProvider.format(params.textDocument.uri) + }.exceptionally { notifyClient(it) } + } + + private fun notifyClient(e: Throwable): T? { + log.error("error", e) + client.join().showMessage(MessageParams(MessageType.Error, e.message)) + return null + } + + override fun didOpen(params: DidOpenTextDocumentParams) { + eventBus.post(DidOpenTextDocumentEvent(params)) + } + + override fun didChange(params: DidChangeTextDocumentParams) { + eventBus.post(DidChangeTextDocumentEvent(params)) + } + + override fun didClose(params: DidCloseTextDocumentParams) { + eventBus.post(DidCloseTextDocumentEvent(params)) + } + + override fun didSave(params: DidSaveTextDocumentParams) { + eventBus.post(DidSaveTextDocumentEvent(params)) + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.java deleted file mode 100644 index 8515f65a..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.java +++ /dev/null @@ -1,202 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import com.google.common.collect.ImmutableList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -import org.eclipse.lsp4j.DidChangeConfigurationParams; -import org.eclipse.lsp4j.DidChangeWatchedFilesParams; -import org.eclipse.lsp4j.DidChangeWatchedFilesRegistrationOptions; -import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; -import org.eclipse.lsp4j.ExecuteCommandOptions; -import org.eclipse.lsp4j.ExecuteCommandParams; -import org.eclipse.lsp4j.FileSystemWatcher; -import org.eclipse.lsp4j.InitializeParams; -import org.eclipse.lsp4j.MessageParams; -import org.eclipse.lsp4j.MessageType; -import org.eclipse.lsp4j.Registration; -import org.eclipse.lsp4j.RegistrationParams; -import org.eclipse.lsp4j.RelativePattern; -import org.eclipse.lsp4j.ServerCapabilities; -import org.eclipse.lsp4j.Unregistration; -import org.eclipse.lsp4j.UnregistrationParams; -import org.eclipse.lsp4j.WorkspaceFolder; -import org.eclipse.lsp4j.WorkspaceFoldersOptions; -import org.eclipse.lsp4j.WorkspaceServerCapabilities; -import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.WorkspaceService; -import org.greenrobot.eventbus.EventBus; -import org.opencds.cqf.cql.ls.server.Constants; -import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent; -import org.opencds.cqf.cql.ls.server.plugin.CommandContribution; -import org.opencds.cqf.cql.ls.server.utility.Futures; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CqlWorkspaceService implements WorkspaceService { - private static final Logger log = LoggerFactory.getLogger(CqlWorkspaceService.class); - - private static final List basicWatchers = Arrays.asList("**/cql-options.json", "ig.ini"); - - private final CompletableFuture client; - private final CompletableFuture> commandContributions; - private final List workspaceFolders; - private final EventBus eventBus; - - public CqlWorkspaceService( - CompletableFuture client, - CompletableFuture> commandContributions, - List workspaceFolders, - EventBus eventBus) { - this.client = client; - this.commandContributions = commandContributions; - this.workspaceFolders = workspaceFolders; - this.eventBus = eventBus; - } - - @SuppressWarnings("java:S125") // Keeping the commented code for future reference - public void initialize(InitializeParams params, ServerCapabilities serverCapabilities) { - this.addFolders(params.getWorkspaceFolders()); - - WorkspaceServerCapabilities wsc = new WorkspaceServerCapabilities(); - - // Register for workspace change notifications - WorkspaceFoldersOptions wfo = new WorkspaceFoldersOptions(); - wfo.setChangeNotifications(true); - wsc.setWorkspaceFolders(wfo); - - // Register for file change notifications - // FileOperationsServerCapabilities fosc = new - // FileOperationsServerCapabilities(); - // wsc.setFileOperations(fosc); - - // Project symbol search - // serverCapabilities.setWorkspaceSymbolProvider(true); - - // Set workspace capabilities - serverCapabilities.setWorkspace(wsc); - - // Register commands - serverCapabilities.setExecuteCommandProvider(new ExecuteCommandOptions(this.getSupportedCommands())); - } - - public void initialized() { - // Add startup logic here. For example, subscribe the EventBus - - this.client - .join() - .unregisterCapability(new UnregistrationParams(Arrays.asList(new Unregistration( - Constants.WORKSPACE_DID_CHANGE_WATCHED_FILES_ID, - Constants.WORKSPACE_DID_CHANGE_WATCHED_FILES_METHOD)))); - - List watchers = basicWatchers.stream() - .map(Either::forLeft) - .map(FileSystemWatcher::new) - .collect(Collectors.toList()); - DidChangeWatchedFilesRegistrationOptions registrationOptions = - new DidChangeWatchedFilesRegistrationOptions(watchers); - - this.client - .join() - .registerCapability(new RegistrationParams(Arrays.asList(new Registration( - Constants.WORKSPACE_DID_CHANGE_WATCHED_FILES_ID, - Constants.WORKSPACE_DID_CHANGE_WATCHED_FILES_METHOD, - registrationOptions)))); - } - - @Override - public CompletableFuture executeCommand(ExecuteCommandParams params) { - try { - return this.executeCommandFromContributions(params); - } catch (Exception e) { - log.error(String.format("executeCommand for %s", params.getCommand()), e); - this.client - .join() - .showMessage(new MessageParams( - MessageType.Error, - String.format("Command %s failed with: %s", params.getCommand(), e.getMessage()))); - return Futures.failed(e); - } - } - - @Override - public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { - try { - this.addFolders(params.getEvent().getAdded()); - this.removeFolders(params.getEvent().getRemoved()); - } catch (Exception e) { - log.error("didChangeWorkspaceFolders", e); - } - } - - @Override - public void didChangeConfiguration(DidChangeConfigurationParams params) { - // No extension configuration as of yet - } - - @Override - public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { - eventBus.post(new DidChangeWatchedFilesEvent(params)); - } - - private void addFolders(List folders) { - if (folders == null) { - return; - } - - for (WorkspaceFolder f : folders) { - workspaceFolders.add(f); - } - } - - private void removeFolders(List folders) { - if (folders == null) { - return; - } - - for (WorkspaceFolder f : folders) { - this.workspaceFolders.remove(f); - } - } - - protected CompletableFuture executeCommandFromContributions(ExecuteCommandParams params) { - String command = params.getCommand(); - - for (CommandContribution commandContribution : this.commandContributions.join()) { - if (commandContribution.getCommands().contains(command)) { - return commandContribution.executeCommand(params); - } - } - - this.client - .join() - .showMessage(new MessageParams(MessageType.Error, String.format("Unknown Command %s", command))); - return CompletableFuture.completedFuture(null); - } - - public List getSupportedCommands() { - Set commands = new HashSet<>(); - for (CommandContribution commandContribution : this.commandContributions.join()) { - if (commandContribution.getCommands() != null) { - for (String command : commandContribution.getCommands()) { - if (commands.contains(command)) { - throw new IllegalArgumentException( - String.format("The command %s was contributed multiple times", command)); - } - - commands.add(command); - } - } - } - - return ImmutableList.copyOf(commands); - } - - public void stop() { - // Add shutdown logic here. For example, unsubscribe the EventBus - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt new file mode 100644 index 00000000..f9efe75d --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt @@ -0,0 +1,166 @@ +package org.opencds.cqf.cql.ls.server.service + +import com.google.common.collect.ImmutableList +import org.eclipse.lsp4j.DidChangeConfigurationParams +import org.eclipse.lsp4j.DidChangeWatchedFilesParams +import org.eclipse.lsp4j.DidChangeWatchedFilesRegistrationOptions +import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams +import org.eclipse.lsp4j.ExecuteCommandOptions +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.FileSystemWatcher +import org.eclipse.lsp4j.InitializeParams +import org.eclipse.lsp4j.MessageParams +import org.eclipse.lsp4j.MessageType +import org.eclipse.lsp4j.Registration +import org.eclipse.lsp4j.RegistrationParams +import org.eclipse.lsp4j.RelativePattern +import org.eclipse.lsp4j.ServerCapabilities +import org.eclipse.lsp4j.Unregistration +import org.eclipse.lsp4j.UnregistrationParams +import org.eclipse.lsp4j.WorkspaceFolder +import org.eclipse.lsp4j.WorkspaceFoldersOptions +import org.eclipse.lsp4j.WorkspaceServerCapabilities +import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.services.WorkspaceService +import org.greenrobot.eventbus.EventBus +import org.opencds.cqf.cql.ls.server.Constants +import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent +import org.opencds.cqf.cql.ls.server.plugin.CommandContribution +import org.opencds.cqf.cql.ls.server.utility.Futures +import org.slf4j.LoggerFactory +import java.util.concurrent.CompletableFuture + +class CqlWorkspaceService( + private val client: CompletableFuture, + private val commandContributions: CompletableFuture>, + private val workspaceFolders: MutableList, + private val eventBus: EventBus +) : WorkspaceService { + + companion object { + private val log = LoggerFactory.getLogger(CqlWorkspaceService::class.java) + private val basicWatchers = listOf("**/cql-options.json", "ig.ini") + } + + @Suppress("java:S125") // Keeping the commented code for future reference + fun initialize(params: InitializeParams, serverCapabilities: ServerCapabilities) { + addFolders(params.workspaceFolders) + + val wsc = WorkspaceServerCapabilities() + + // Register for workspace change notifications + val wfo = WorkspaceFoldersOptions() + wfo.setChangeNotifications(true) + wsc.workspaceFolders = wfo + + // Register for file change notifications + // FileOperationsServerCapabilities fosc = new FileOperationsServerCapabilities(); + // wsc.setFileOperations(fosc); + + // Project symbol search + // serverCapabilities.setWorkspaceSymbolProvider(true); + + // Set workspace capabilities + serverCapabilities.workspace = wsc + + // Register commands + serverCapabilities.executeCommandProvider = ExecuteCommandOptions(getSupportedCommands()) + } + + fun initialized() { + // Add startup logic here. For example, subscribe the EventBus + + client.join().unregisterCapability( + UnregistrationParams( + listOf(Unregistration( + Constants.WORKSPACE_DID_CHANGE_WATCHED_FILES_ID, + Constants.WORKSPACE_DID_CHANGE_WATCHED_FILES_METHOD + )) + ) + ) + + val watchers = basicWatchers + .map { Either.forLeft(it) } + .map { FileSystemWatcher(it) } + + val registrationOptions = DidChangeWatchedFilesRegistrationOptions(watchers) + + client.join().registerCapability( + RegistrationParams( + listOf(Registration( + Constants.WORKSPACE_DID_CHANGE_WATCHED_FILES_ID, + Constants.WORKSPACE_DID_CHANGE_WATCHED_FILES_METHOD, + registrationOptions + )) + ) + ) + } + + override fun executeCommand(params: ExecuteCommandParams): CompletableFuture { + return try { + executeCommandFromContributions(params) + } catch (e: Exception) { + log.error("executeCommand for ${params.command}", e) + client.join().showMessage( + MessageParams(MessageType.Error, "Command ${params.command} failed with: ${e.message}") + ) + Futures.failed(e) + } + } + + override fun didChangeWorkspaceFolders(params: DidChangeWorkspaceFoldersParams) { + try { + addFolders(params.event.added) + removeFolders(params.event.removed) + } catch (e: Exception) { + log.error("didChangeWorkspaceFolders", e) + } + } + + override fun didChangeConfiguration(params: DidChangeConfigurationParams) { + // No extension configuration as of yet + } + + override fun didChangeWatchedFiles(params: DidChangeWatchedFilesParams) { + eventBus.post(DidChangeWatchedFilesEvent(params)) + } + + private fun addFolders(folders: List?) { + folders?.forEach { workspaceFolders.add(it) } + } + + private fun removeFolders(folders: List?) { + folders?.forEach { workspaceFolders.remove(it) } + } + + protected fun executeCommandFromContributions(params: ExecuteCommandParams): CompletableFuture { + val command = params.command + + for (commandContribution in commandContributions.join()) { + if (commandContribution.getCommands().contains(command)) { + return commandContribution.executeCommand(params) + } + } + + client.join().showMessage(MessageParams(MessageType.Error, "Unknown Command $command")) + return CompletableFuture.completedFuture(null) + } + + fun getSupportedCommands(): List { + val commands = mutableSetOf() + for (commandContribution in commandContributions.join()) { + for (command in commandContribution.getCommands()) { + if (commands.contains(command)) { + throw IllegalArgumentException("The command $command was contributed multiple times") + } + commands.add(command) + } + } + return ImmutableList.copyOf(commands) + } + + fun stop() { + // Add shutdown logic here. For example, unsubscribe the EventBus + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.java deleted file mode 100644 index 09088b4c..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.java +++ /dev/null @@ -1,221 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.base.Joiner; -import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import org.apache.commons.lang3.tuple.Pair; -import org.cqframework.cql.cql2elm.CqlCompiler; -import org.cqframework.cql.cql2elm.CqlCompilerException; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.DiagnosticSeverity; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.PublishDiagnosticsParams; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.services.LanguageClient; -import org.greenrobot.eventbus.Subscribe; -import org.hl7.elm.r1.VersionedIdentifier; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.core.utility.Uris; -import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.utility.Diagnostics; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DiagnosticsService { - - private static Logger log = LoggerFactory.getLogger(DiagnosticsService.class); - - private static final long BOUNCE_DELAY = 200; - private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(x -> { - Thread t = new Thread(x, "Debouncer"); - t.setDaemon(true); - return t; - }); - - private ScheduledFuture future; - - private CqlCompilationManager cqlCompilationManager; - private CompletableFuture client; - private ContentService contentService; - - public DiagnosticsService( - CompletableFuture client, - CqlCompilationManager cqlCompilationManager, - ContentService contentService) { - this.client = client; - this.cqlCompilationManager = cqlCompilationManager; - this.contentService = contentService; - } - - protected void doLint(Collection paths) { - if (paths == null || paths.isEmpty()) { - return; - } - - log.debug("Lint: {}", Joiner.on(", ").join(paths)); - - Map> allDiagnostics = new HashMap<>(); - for (URI uri : paths) { - Map> currentDiagnostics = this.lint(uri); - log.debug("Merging Diagnostics: {}", uri); - this.mergeDiagnostics(allDiagnostics, currentDiagnostics); - } - - for (Map.Entry> entry : allDiagnostics.entrySet()) { - log.debug("Publishing {} Diagnostics for: {}", entry.getValue().size(), entry.getKey()); - PublishDiagnosticsParams params = - new PublishDiagnosticsParams(Uris.toClientUri(entry.getKey()), new ArrayList<>(entry.getValue())); - client.join().publishDiagnostics(params); - } - } - - private void mergeDiagnostics( - Map> currentDiagnostics, Map> newDiagnostics) { - checkNotNull(currentDiagnostics); - checkNotNull(newDiagnostics); - - for (Entry> entry : newDiagnostics.entrySet()) { - Set currentSet = currentDiagnostics.computeIfAbsent(entry.getKey(), k -> new HashSet<>()); - for (Diagnostic d : entry.getValue()) { - currentSet.add(d); - } - } - } - - public Map> lint(URI uri) { - Map> diagnostics = new HashMap<>(); - CqlCompiler compiler = this.cqlCompilationManager.compile(uri); - if (compiler == null) { - Diagnostic d = new Diagnostic( - new Range(new Position(0, 0), new Position(0, 0)), - "Library does not contain CQL content.", - DiagnosticSeverity.Warning, - "lint"); - - diagnostics.computeIfAbsent(uri, k -> new HashSet<>()).add(d); - - return diagnostics; - } - - List exceptions = compiler.getExceptions(); - - log.debug("lint completed on {} with {} messages.", uri, exceptions.size()); - - List uniqueLibraries = exceptions.stream() - .map(x -> x == null || x.getLocator() == null - ? null - : x.getLocator().getLibrary()) - .distinct() - .filter(x -> x != null && x.getId() != null) - .collect(Collectors.toList()); - - URI root = Uris.getHead(uri); - var libraryUriList = new ArrayList>(); - for (var libraryIdentifier : uniqueLibraries) { - var uris = this.contentService.locate(root, libraryIdentifier); - if (uris != null && !uris.isEmpty()) { - libraryUriList.add(Pair.of(libraryIdentifier, uris.iterator().next())); - } else { - // The message is associated with a library loaded from outside the content service (e.g. an npm - // library) - // So associate the message with the current uri - libraryUriList.add(Pair.of(libraryIdentifier, uri)); - } - } - - Map libraryUris = new HashMap<>(); - for (Pair p : libraryUriList) { - libraryUris.put(p.getLeft(), p.getRight()); - } - - // Map "unknown" libraries to the current uri - libraryUris.put(new VersionedIdentifier().withId("unknown"), uri); - - for (CqlCompilerException exception : exceptions) { - if (exception != null) { - URI eUri = exception.getLocator() != null - && exception.getLocator().getLibrary() != null - && exception.getLocator().getLibrary().getId() != null - ? libraryUris.get(exception.getLocator().getLibrary()) - : null; - if (eUri == null) { - eUri = uri; // put all unknown or indeterminate errors to the current uri so at least they get - // reported - } - - Diagnostic d = Diagnostics.convert(exception); - - if (d != null) { - log.debug( - "diagnostic: {} {}:{}-{}:{}: {}", - eUri, - d.getRange().getStart().getLine(), - d.getRange().getStart().getCharacter(), - d.getRange().getEnd().getLine(), - d.getRange().getEnd().getCharacter(), - d.getMessage()); - - diagnostics.computeIfAbsent(eUri, k -> new HashSet<>()).add(d); - } - } - } - - // Ensure there is an entry for the library in the case that there are no - // exceptions - diagnostics.computeIfAbsent(uri, k -> new HashSet<>()); - - return diagnostics; - } - - @Subscribe - public void didOpen(DidOpenTextDocumentEvent e) { - log.debug("didOpen: {}", e.params().getTextDocument().getUri()); - - doLint(Collections.singletonList( - Uris.parseOrNull(e.params().getTextDocument().getUri()))); - } - - @Subscribe - public void didClose(DidCloseTextDocumentEvent e) { - log.debug("didClose: {}", e.params().getTextDocument().getUri()); - - PublishDiagnosticsParams params = - new PublishDiagnosticsParams(e.params().getTextDocument().getUri(), new ArrayList<>()); - client.join().publishDiagnostics(params); - } - - @Subscribe - public void didChange(DidChangeTextDocumentEvent e) { - log.debug("didChange: {}", e.params().getTextDocument().getUri()); - - debounce( - BOUNCE_DELAY, - () -> doLint(Collections.singletonList( - Uris.parseOrNull(e.params().getTextDocument().getUri())))); - } - - void debounce(long delay, Runnable task) { - if (this.future != null && !this.future.isDone()) this.future.cancel(false); - - this.future = this.executor.schedule(task, delay, TimeUnit.MILLISECONDS); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt new file mode 100644 index 00000000..91f6de9b --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt @@ -0,0 +1,178 @@ +package org.opencds.cqf.cql.ls.server.service + +import com.google.common.base.Joiner +import com.google.common.base.Preconditions.checkNotNull +import org.cqframework.cql.cql2elm.CqlCompilerException +import org.eclipse.lsp4j.Diagnostic +import org.eclipse.lsp4j.DiagnosticSeverity +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.PublishDiagnosticsParams +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.services.LanguageClient +import org.greenrobot.eventbus.Subscribe +import org.hl7.elm.r1.VersionedIdentifier +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.utility.Diagnostics +import org.slf4j.LoggerFactory +import java.net.URI +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +class DiagnosticsService( + private val client: CompletableFuture, + private val cqlCompilationManager: CqlCompilationManager, + private val contentService: ContentService +) { + + companion object { + private val log = LoggerFactory.getLogger(DiagnosticsService::class.java) + private const val BOUNCE_DELAY = 200L + } + + private val executor = Executors.newSingleThreadScheduledExecutor { r -> + Thread(r, "Debouncer").also { it.isDaemon = true } + } + + private var future: ScheduledFuture<*>? = null + + protected fun doLint(paths: Collection?) { + if (paths == null || paths.isEmpty()) return + + log.debug("Lint: {}", Joiner.on(", ").join(paths)) + + val allDiagnostics = mutableMapOf>() + for (uri in paths) { + val currentDiagnostics = lint(uri) + log.debug("Merging Diagnostics: {}", uri) + mergeDiagnostics(allDiagnostics, currentDiagnostics) + } + + for ((uri, diagnostics) in allDiagnostics) { + log.debug("Publishing {} Diagnostics for: {}", diagnostics.size, uri) + val params = PublishDiagnosticsParams(Uris.toClientUri(uri), ArrayList(diagnostics)) + client.join().publishDiagnostics(params) + } + } + + private fun mergeDiagnostics( + currentDiagnostics: MutableMap>, + newDiagnostics: Map> + ) { + checkNotNull(currentDiagnostics) + checkNotNull(newDiagnostics) + + for ((uri, diagnostics) in newDiagnostics) { + currentDiagnostics.getOrPut(uri) { mutableSetOf() }.addAll(diagnostics) + } + } + + fun lint(uri: URI): Map> { + val diagnostics = mutableMapOf>() + val compiler = cqlCompilationManager.compile(uri) + if (compiler == null) { + val d = Diagnostic( + Range(Position(0, 0), Position(0, 0)), + "Library does not contain CQL content.", + DiagnosticSeverity.Warning, + "lint" + ) + diagnostics.getOrPut(uri) { mutableSetOf() }.add(d) + return diagnostics + } + + val exceptions: List = compiler.exceptions + + log.debug("lint completed on {} with {} messages.", uri, exceptions.size) + + val uniqueLibraries: List = exceptions + .map { it?.locator?.library } + .distinct() + .filterNotNull() + .filter { it.id != null } + + val root = Uris.getHead(uri) + val libraryUris = mutableMapOf() + + for (libraryIdentifier in uniqueLibraries) { + val uris = contentService.locate(root, libraryIdentifier) + if (!uris.isNullOrEmpty()) { + libraryUris[libraryIdentifier] = uris.iterator().next() + } else { + // The message is associated with a library loaded from outside the content service (e.g. an npm + // library). So associate the message with the current uri + libraryUris[libraryIdentifier] = uri + } + } + + // Map "unknown" libraries to the current uri + libraryUris[VersionedIdentifier().withId("unknown")] = uri + + for (exception in exceptions) { + if (exception != null) { + val exLocator = exception.locator + val exLibrary = exLocator?.library + var eUri = if (exLibrary != null && exLibrary.id != null) { + libraryUris[exLibrary] + } else null + + if (eUri == null) { + eUri = uri // put all unknown or indeterminate errors to the current uri so at least they get reported + } + + val d = Diagnostics.convert(exception) + + if (d != null) { + log.debug( + "diagnostic: {} {}:{}-{}:{}: {}", + eUri, + d.range.start.line, + d.range.start.character, + d.range.end.line, + d.range.end.character, + d.message + ) + + diagnostics.getOrPut(eUri) { mutableSetOf() }.add(d) + } + } + } + + // Ensure there is an entry for the library in the case that there are no exceptions + diagnostics.getOrPut(uri) { mutableSetOf() } + + return diagnostics + } + + @Subscribe + fun didOpen(e: DidOpenTextDocumentEvent) { + log.debug("didOpen: {}", e.params().textDocument.uri) + doLint(listOfNotNull(Uris.parseOrNull(e.params().textDocument.uri))) + } + + @Subscribe + fun didClose(e: DidCloseTextDocumentEvent) { + log.debug("didClose: {}", e.params().textDocument.uri) + val params = PublishDiagnosticsParams(e.params().textDocument.uri, ArrayList()) + client.join().publishDiagnostics(params) + } + + @Subscribe + fun didChange(e: DidChangeTextDocumentEvent) { + log.debug("didChange: {}", e.params().textDocument.uri) + debounce(BOUNCE_DELAY) { + doLint(listOfNotNull(Uris.parseOrNull(e.params().textDocument.uri))) + } + } + + internal fun debounce(delay: Long, task: Runnable) { + if (future != null && !future!!.isDone) future!!.cancel(false) + future = executor.schedule(task, delay, TimeUnit.MILLISECONDS) + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.java deleted file mode 100644 index 73124853..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import static com.google.common.base.Preconditions.checkNotNull; - -import java.io.InputStream; -import java.net.URI; -import java.util.Set; -import org.hl7.elm.r1.VersionedIdentifier; -import org.opencds.cqf.cql.ls.core.ContentService; - -public class FederatedContentService implements ContentService { - - private ActiveContentService activeContentService; - private ContentService fileContentService; - - public FederatedContentService(ActiveContentService activeContentService, ContentService fileContentService) { - this.activeContentService = activeContentService; - this.fileContentService = fileContentService; - } - - @Override - public Set locate(URI root, VersionedIdentifier identifier) { - checkNotNull(root); - checkNotNull(identifier); - - Set locations = this.activeContentService.locate(root, identifier); - - locations.addAll(this.fileContentService.locate(root, identifier)); - - return locations; - } - - @Override - public InputStream read(URI uri) { - checkNotNull(uri); - - if (this.activeContentService.activeUris().contains(uri)) { - return this.activeContentService.read(uri); - } - - return this.fileContentService.read(uri); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.kt new file mode 100644 index 00000000..7c2cc9da --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.kt @@ -0,0 +1,30 @@ +package org.opencds.cqf.cql.ls.server.service + +import com.google.common.base.Preconditions.checkNotNull +import org.hl7.elm.r1.VersionedIdentifier +import org.opencds.cqf.cql.ls.core.ContentService +import java.io.InputStream +import java.net.URI + +class FederatedContentService( + private val activeContentService: ActiveContentService, + private val fileContentService: ContentService +) : ContentService { + + override fun locate(root: URI, identifier: VersionedIdentifier): Set { + checkNotNull(root) + checkNotNull(identifier) + val locations = activeContentService.locate(root, identifier).toMutableSet() + locations.addAll(fileContentService.locate(root, identifier)) + return locations + } + + override fun read(uri: URI): InputStream? { + checkNotNull(uri) + return if (activeContentService.activeUris().contains(uri)) { + activeContentService.read(uri) + } else { + fileContentService.read(uri) + } + } +} 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 deleted file mode 100644 index 1a56996e..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.java +++ /dev/null @@ -1,194 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import static com.google.common.base.Preconditions.checkNotNull; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.filefilter.IOFileFilter; -import org.apache.commons.io.filefilter.TrueFileFilter; -import org.apache.commons.lang3.tuple.Pair; -import org.cqframework.cql.cql2elm.model.Version; -import org.eclipse.lsp4j.WorkspaceFolder; -import org.hl7.elm.r1.VersionedIdentifier; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.core.utility.Uris; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -// NOTE: This implementation is naive and assumes library file names will always take the form: -// [-].cql -// 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); - - protected final List workspaceFolders; - - public FileContentService(List workspaceFolders) { - this.workspaceFolders = workspaceFolders; - } - - @Override - public Set locate(URI root, VersionedIdentifier identifier) { - checkNotNull(root); - checkNotNull(identifier); - - Set uris = new HashSet<>(); - - // This just checks to see if the requested - // location URI is part of the workspace. - // If not, no locations are returned. - for (WorkspaceFolder w : this.workspaceFolders) { - URI folderUri = Uris.parseOrNull(w.getUri()); - // If root is not a is a child of the workspace folder, skip it. - if (folderUri == null || folderUri.relativize(root).equals(root)) { - continue; - } - - File file = searchFolder(root, identifier); - if (file != null && file.exists()) { - uris.add(file.toURI()); - } - } - - return uris; - } - - public static File searchFolder(URI directory, VersionedIdentifier libraryIdentifier) { - Path path; - try { - path = Paths.get(directory); - } catch (Exception e) { - log.warn(String.format("error searching directory %s. Skipping.", directory), e); - return null; - } - - // First, try a direct match - String libraryName = libraryIdentifier.getId(); - Path libraryPath = path.resolve(String.format( - "%s%s.cql", - libraryName, libraryIdentifier.getVersion() != null ? ("-" + libraryIdentifier.getVersion()) : "")); - File libraryFile = libraryPath.toFile(); - - if (libraryFile.exists()) { - return libraryFile; - } else { - return nearestMatch(path, libraryIdentifier.getId(), libraryIdentifier.getVersion()); - } - } - - private static IOFileFilter ioFilter(String name) { - return new IOFileFilter() { - - @Override - public boolean accept(File dir, String filename) { - return filename.startsWith(name) && filename.endsWith(".cql"); - } - - @Override - public boolean accept(File file) { - if (file.isFile()) { - return this.accept(file, file.getName()); - } else { - return false; - } - } - }; - } - - private static File nearestMatch(Path directory, String name, String version) { - Collection files = FileUtils.listFiles(directory.toFile(), ioFilter(name), TrueFileFilter.INSTANCE); - if (files == null || files.isEmpty()) { - return null; - } - - File mostRecentFile = null; - Version mostRecent = null; - Version requestedVersion = version == null ? null : new Version(version); - - // The filter will give us all the files that start with the appropriate the - // appropriate name. We then need to - // split apart the name and version to do a more detailed comparison - // e.g. for a request for patient-view version 1.0.0 we might see: - // patient-view.cql - // patient-view-demo.cql - // patient-view-1.0.1.cql - // patient-view-1.0.0.cql - // patient-view-demo-1.0.0.cql - - for (File file : files) { - String fileName = file.getName(); - Pair nameAndVersion = getNameAndVersion(fileName); - - if (!nameAndVersion.getLeft().equalsIgnoreCase(name)) { - continue; - } - - Version v = nameAndVersion.getRight(); - - // Exact match - if (v != null && requestedVersion != null && v.compareTo(requestedVersion) == 0) { - return file; - } - // If the file is named correctly but has no version, consider it the most - // recent version - else if (v == null) { - return file; - } - // Otherwise, find the most recent compatible version - else if ((requestedVersion == null || v.compatibleWith(requestedVersion)) - && (mostRecent == null || v.compareTo(mostRecent) > 0)) { - mostRecent = v; - mostRecentFile = file; - } - } - - return mostRecentFile; - } - - private static Version getVersion(String version) { - try { - return new Version(version); - } catch (Exception e) { - return null; - } - } - - private static Pair getNameAndVersion(String fileName) { - int indexOfExtension = fileName.lastIndexOf("."); - if (indexOfExtension >= 0) { - fileName = fileName.substring(0, indexOfExtension); - } - - int indexOfVersionSeparator = fileName.lastIndexOf("-"); - Version version = null; - if (indexOfVersionSeparator >= 0) { - version = getVersion(fileName.substring(indexOfVersionSeparator + 1)); - if (version != null) { - fileName = fileName.substring(0, indexOfVersionSeparator); - } - } - - return Pair.of(fileName, version); - } - - @Override - public InputStream read(URI uri) { - try { - return new BufferedInputStream(new FileInputStream(new File(uri))); - } catch (FileNotFoundException e) { - return null; - } - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.kt new file mode 100644 index 00000000..70a5a8ef --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.kt @@ -0,0 +1,123 @@ +package org.opencds.cqf.cql.ls.server.service + +import com.google.common.base.Preconditions.checkNotNull +import org.apache.commons.io.FileUtils +import org.apache.commons.io.filefilter.IOFileFilter +import org.apache.commons.io.filefilter.TrueFileFilter +import org.cqframework.cql.cql2elm.model.Version +import org.eclipse.lsp4j.WorkspaceFolder +import org.hl7.elm.r1.VersionedIdentifier +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.slf4j.LoggerFactory +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream +import java.io.InputStream +import java.net.URI +import java.nio.file.Paths + +// NOTE: This implementation is naive and assumes library file names will always take the form: +// [-].cql +class FileContentService(protected val workspaceFolders: List) : ContentService { + + companion object { + private val log = LoggerFactory.getLogger(FileContentService::class.java) + + @JvmStatic + fun searchFolder(directory: URI, libraryIdentifier: VersionedIdentifier): File? { + val path = try { + Paths.get(directory) + } catch (e: Exception) { + log.warn("error searching directory $directory. Skipping.", e) + return null + } + + val libraryName = libraryIdentifier.id ?: return null + val libraryPath = path.resolve( + "$libraryName${if (libraryIdentifier.version != null) "-${libraryIdentifier.version}" else ""}.cql" + ) + val libraryFile = libraryPath.toFile() + return if (libraryFile.exists()) libraryFile + else nearestMatch(path.toFile(), libraryName, libraryIdentifier.version) + } + + private fun ioFilter(name: String): IOFileFilter = object : IOFileFilter { + override fun accept(dir: File, filename: String) = + filename.startsWith(name) && filename.endsWith(".cql") + + override fun accept(file: File) = + if (file.isFile) accept(file, file.name) else false + } + + private fun nearestMatch(directory: File, name: String, version: String?): File? { + val files = FileUtils.listFiles(directory, ioFilter(name), TrueFileFilter.INSTANCE) + if (files == null || files.isEmpty()) return null + + var mostRecentFile: File? = null + var mostRecent: Version? = null + val requestedVersion = if (version != null) try { Version(version) } catch (e: Exception) { null } else null + + for (file in files) { + val fileName = file.name + val (parsedName, v) = getNameAndVersion(fileName) + + if (!parsedName.equals(name, ignoreCase = true)) continue + + when { + v != null && requestedVersion != null && v.compareTo(requestedVersion) == 0 -> return file + v == null -> return file + (requestedVersion == null || v.compatibleWith(requestedVersion)) && + (mostRecent == null || v.compareTo(mostRecent) > 0) -> { + mostRecent = v + mostRecentFile = file + } + } + } + return mostRecentFile + } + + private fun getVersion(version: String): Version? { + return try { Version(version) } catch (e: Exception) { null } + } + + private fun getNameAndVersion(fileName: String): Pair { + var name = fileName + val indexOfExtension = name.lastIndexOf(".") + if (indexOfExtension >= 0) name = name.substring(0, indexOfExtension) + + val indexOfVersionSeparator = name.lastIndexOf("-") + var version: Version? = null + if (indexOfVersionSeparator >= 0) { + version = getVersion(name.substring(indexOfVersionSeparator + 1)) + if (version != null) name = name.substring(0, indexOfVersionSeparator) + } + return Pair(name, version) + } + } + + override fun locate(root: URI, identifier: VersionedIdentifier): Set { + checkNotNull(root) + checkNotNull(identifier) + + val uris = mutableSetOf() + for (w in workspaceFolders) { + val folderUri = Uris.parseOrNull(w.uri) ?: continue + if (folderUri.relativize(root) == root) continue + + val file = searchFolder(root, identifier) + if (file != null && file.exists()) { + uris.add(file.toURI()) + } + } + return uris + } + + override fun read(uri: URI): InputStream? { + return try { + BufferedInputStream(FileInputStream(File(uri))) + } catch (e: Exception) { + null + } + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.java deleted file mode 100644 index f56856c5..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.opencds.cqf.cql.ls.server.utility; - -import java.util.HashSet; -import java.util.Set; -import org.cqframework.cql.cql2elm.CqlCompilerException; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.DiagnosticSeverity; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; - -public class Diagnostics { - private Diagnostics() {} - - public static Diagnostic convert(CqlCompilerException error) { - if (error.getLocator() != null) { - Range range = position(error); - Diagnostic diagnostic = new Diagnostic(); - DiagnosticSeverity severity = severity(error.getSeverity()); - - diagnostic.setSeverity(severity); - diagnostic.setRange(range); - diagnostic.setMessage(error.getMessage()); - - return diagnostic; - } else { - return null; - } - } - - public static Set convert(Iterable errors) { - Set result = new HashSet<>(); - for (CqlCompilerException error : errors) { - Diagnostic diagnostic = convert(error); - if (diagnostic != null) { - result.add(diagnostic); - } - } - return result; - } - - private static DiagnosticSeverity severity(CqlCompilerException.ErrorSeverity severity) { - switch (severity) { - case Error: - return DiagnosticSeverity.Error; - case Warning: - return DiagnosticSeverity.Warning; - case Info: - default: - return DiagnosticSeverity.Information; - } - } - - private static Range position(CqlCompilerException error) { - // The Language server API assumes 0 based indices and an exclusive range - return new Range( - new Position( - Math.max(error.getLocator().getStartLine() - 1, 0), - Math.max(error.getLocator().getStartChar() - 1, 0)), - new Position( - Math.max(error.getLocator().getEndLine() - 1, 0), - Math.max(error.getLocator().getEndChar(), 0))); - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt new file mode 100644 index 00000000..f0ae1df9 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt @@ -0,0 +1,52 @@ +package org.opencds.cqf.cql.ls.server.utility + +import org.cqframework.cql.cql2elm.CqlCompilerException +import org.eclipse.lsp4j.Diagnostic +import org.eclipse.lsp4j.DiagnosticSeverity +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range + +object Diagnostics { + @JvmStatic + fun convert(error: CqlCompilerException): Diagnostic? { + if (error.locator == null) return null + val range = position(error) + val diagnostic = Diagnostic() + diagnostic.severity = severity(error.severity) + diagnostic.range = range + diagnostic.message = error.message + return diagnostic + } + + @JvmStatic + fun convert(errors: Iterable): Set { + val result = mutableSetOf() + for (error in errors) { + val diagnostic = convert(error) + if (diagnostic != null) result.add(diagnostic) + } + return result + } + + private fun severity(severity: CqlCompilerException.ErrorSeverity): DiagnosticSeverity { + return when (severity) { + CqlCompilerException.ErrorSeverity.Error -> DiagnosticSeverity.Error + CqlCompilerException.ErrorSeverity.Warning -> DiagnosticSeverity.Warning + else -> DiagnosticSeverity.Information + } + } + + private fun position(error: CqlCompilerException): Range { + val locator = error.locator!! + return Range( + Position( + maxOf(locator.startLine - 1, 0), + maxOf(locator.startChar - 1, 0) + ), + Position( + maxOf(locator.endLine - 1, 0), + maxOf(locator.endChar, 0) + ) + ) + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Futures.java b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Futures.java deleted file mode 100644 index 613aeef2..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Futures.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.opencds.cqf.cql.ls.server.utility; - -import static com.google.common.base.Preconditions.checkNotNull; - -import java.util.concurrent.CompletableFuture; - -public class Futures { - private Futures() {} - - public static CompletableFuture failed(Throwable ex) { - checkNotNull(ex); - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(ex); - return future; - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Futures.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Futures.kt new file mode 100644 index 00000000..75ff440d --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Futures.kt @@ -0,0 +1,13 @@ +package org.opencds.cqf.cql.ls.server.utility + +import java.util.concurrent.CompletableFuture + +object Futures { + @JvmStatic + fun failed(ex: Throwable): CompletableFuture { + requireNotNull(ex) + val future = CompletableFuture() + future.completeExceptionally(ex) + return future + } +} 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 deleted file mode 100644 index 5fbd0f5d..00000000 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.java +++ /dev/null @@ -1,79 +0,0 @@ -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; -import org.hl7.elm.r1.Retrieve; -import org.jetbrains.annotations.NotNull; - -public class ExpressionTrackBackVisitor extends BaseElmLibraryVisitor { - - // Return the child result if it's not null (IOW, it's more specific than this result). - // Otherwise, return the current result - @Override - protected Element aggregateResult(Element aggregate, Element nextResult) { - if (nextResult != null) { - return nextResult; - } - - return aggregate; - } - - @Override - public Element visitExpressionDef(ExpressionDef elm, TrackBack context) { - Element childResult = super.visitExpressionDef(elm, context); - return aggregateResult(elementCoversTrackBack(elm, context) ? elm : null, childResult); - } - - @Override - public Element visitRetrieve(Retrieve retrieve, TrackBack context) { - if (elementCoversTrackBack(retrieve, context)) { - return retrieve; - } - - return null; - } - - protected boolean elementCoversTrackBack(Element elm, TrackBack context) { - for (TrackBack tb : Trackable.INSTANCE.getTrackbacks(elm)) { - if (startsOnOrBefore(tb, context) && endsOnOrAfter(tb, context)) { - return true; - } - } - - return false; - } - - protected boolean startsOnOrBefore(TrackBack left, TrackBack right) { - if (left.getStartLine() > right.getStartLine()) { - return false; - } - - if (left.getStartLine() < right.getStartLine()) { - return true; - } - - // Same line - return left.getStartChar() <= right.getStartChar(); - } - - protected boolean endsOnOrAfter(TrackBack left, TrackBack right) { - if (left.getEndLine() < right.getEndLine()) { - return false; - } - - if (left.getEndLine() > right.getEndLine()) { - return true; - } - - // Same line - return left.getEndChar() >= right.getEndChar(); - } - - @Override - protected Element defaultResult(@NotNull Element element, TrackBack trackBack) { - return null; - } -} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.kt new file mode 100644 index 00000000..0bf49c89 --- /dev/null +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.kt @@ -0,0 +1,51 @@ +package org.opencds.cqf.cql.ls.server.visitor + +import org.cqframework.cql.cql2elm.tracking.TrackBack +import org.cqframework.cql.cql2elm.tracking.Trackable.trackbacks +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 + +open class ExpressionTrackBackVisitor : BaseElmLibraryVisitor() { + + // Return the child result if it's not null (i.e., it's more specific than the current result). + // Otherwise, return the current result. + override fun aggregateResult(aggregate: Element?, nextResult: Element?): Element? { + return if (nextResult != null) nextResult else aggregate + } + + override fun visitExpressionDef(elm: ExpressionDef, context: TrackBack?): Element? { + val childResult = super.visitExpressionDef(elm, context) + return aggregateResult(if (context != null && elementCoversTrackBack(elm, context)) elm else null, childResult) + } + + override fun visitRetrieve(retrieve: Retrieve, context: TrackBack?): Element? { + return if (context != null && elementCoversTrackBack(retrieve, context)) retrieve else null + } + + protected fun elementCoversTrackBack(elm: Element, context: TrackBack): Boolean { + for (tb in elm.trackbacks) { + if (startsOnOrBefore(tb, context) && endsOnOrAfter(tb, context)) { + return true + } + } + return false + } + + protected fun startsOnOrBefore(left: TrackBack, right: TrackBack): Boolean { + if (left.startLine > right.startLine) return false + if (left.startLine < right.startLine) return true + // Same line + return left.startChar <= right.startChar + } + + protected fun endsOnOrAfter(left: TrackBack, right: TrackBack): Boolean { + if (left.endLine < right.endLine) return false + if (left.endLine > right.endLine) return true + // Same line + return left.endChar >= right.endChar + } + + override fun defaultResult(element: Element, trackBack: TrackBack?): Element? = null +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/LanguageServerTest.kt b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/LanguageServerTest.kt new file mode 100644 index 00000000..af690657 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/LanguageServerTest.kt @@ -0,0 +1,88 @@ +package org.opencds.cqf.cql.ls.server + +import org.eclipse.lsp4j.HoverParams +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.services.LanguageClient +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Logger.JavaLogger +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import org.opencds.cqf.cql.ls.server.plugin.CommandContribution +import org.opencds.cqf.cql.ls.server.provider.FormattingProvider +import org.opencds.cqf.cql.ls.server.provider.HoverProvider +import org.opencds.cqf.cql.ls.server.service.CqlTextDocumentService +import org.opencds.cqf.cql.ls.server.service.CqlWorkspaceService +import org.opencds.cqf.cql.ls.server.service.TestContentService +import java.util.concurrent.CompletableFuture + +class LanguageServerTest { + + companion object { + private lateinit var server: CqlLanguageServer + + @BeforeAll + @JvmStatic + fun beforeAll() { + val eventBus = EventBus.builder().logger(JavaLogger("eventBus")).build() + val cs = TestContentService() + val compilationManager = CqlCompilationManager(cs, CompilerOptionsManager(cs), IgContextManager(cs)) + val languageClientFuture = CompletableFuture() + val commandsFuture = CompletableFuture>() + commandsFuture.complete(emptyList()) + server = CqlLanguageServer( + languageClientFuture, + CqlWorkspaceService(languageClientFuture, commandsFuture, mutableListOf(), eventBus), + CqlTextDocumentService(languageClientFuture, HoverProvider(compilationManager), FormattingProvider(cs), eventBus) + ) + } + } + + @Test + fun handshake() { + assertNotNull(server) + } + + @Test + fun hoverInt() { + val hover = server.getTextDocumentService() + .hover(HoverParams(TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), Position(5, 2))) + .get() + + assertNotNull(hover) + assertNotNull(hover!!.contents.right) + + val markup = hover.contents.right + assertEquals("markdown", markup.kind) + assertEquals("```cql\nSystem.Integer\n```", markup.value) + } + + @Test + fun hoverNothing() { + val hover = server.getTextDocumentService() + .hover(HoverParams(TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), Position(2, 0))) + .get() + + assertNull(hover) + } + + @Test + fun hoverList() { + val hover = server.getTextDocumentService() + .hover(HoverParams(TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), Position(8, 2))) + .get() + + assertNotNull(hover) + assertNotNull(hover!!.contents.right) + + val markup = hover.contents.right + assertEquals("markdown", markup.kind) + assertEquals("```cql\nlist\n```", markup.value) + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContributionTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContributionTest.java deleted file mode 100644 index 71adf71b..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContributionTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.opencds.cqf.cql.ls.server.command; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.google.gson.JsonParser; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.ExecuteCommandParams; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.manager.IgContextManager; -import org.opencds.cqf.cql.ls.server.service.TestContentService; - -class ViewElmCommandContributionTest { - - private static ViewElmCommandContribution viewElmCommandContribution; - private Object expectedJson; - - @BeforeAll - static void beforeAll() { - ContentService cs = new TestContentService(); - CqlCompilationManager cqlCompilationManager = - new CqlCompilationManager(cs, new CompilerOptionsManager(cs), new IgContextManager(cs)); - viewElmCommandContribution = new ViewElmCommandContribution(cqlCompilationManager); - } - - @Test - void getCommands() { - assertEquals(1, viewElmCommandContribution.getCommands().size()); - assertEquals( - "org.opencds.cqf.cql.ls.viewElm", - viewElmCommandContribution.getCommands().toArray()[0]); - } - - @Test - void executeCommand() { - ExecuteCommandParams params = new ExecuteCommandParams(); - params.setCommand("org.opencds.cqf.cql.ls.viewElm"); - params.setArguments(Collections.singletonList( - JsonParser.parseString("\"\\/org\\/opencds\\/cqf\\/cql\\/ls\\/server\\/One.cql\""))); - CompletableFuture future = viewElmCommandContribution - .executeCommand(params) - .thenAccept(result -> { - try { - String expectedXml = new String(Files.readAllBytes( - Paths.get("src/test/resources/org/opencds/cqf/cql/ls/server/One.xml"))) - .trim() - .replaceAll("\\s+", ""); - assertEquals(expectedXml, result.toString().trim().replaceAll("\\s+", "")); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - - // This ensures the test waits and fails if an exception occurs - future.join(); - } - - @Test - void executeCommandWithXmlElmType() { - ExecuteCommandParams params = new ExecuteCommandParams(); - params.setCommand("org.opencds.cqf.cql.ls.viewElm"); - params.setArguments(List.of( - JsonParser.parseString("\"\\/org\\/opencds\\/cqf\\/cql\\/ls\\/server\\/One.cql\""), - JsonParser.parseString("\"xml\""))); - CompletableFuture future = viewElmCommandContribution - .executeCommand(params) - .thenAccept(result -> { - try { - String expectedXml = new String(Files.readAllBytes( - Paths.get("src/test/resources/org/opencds/cqf/cql/ls/server/One.xml"))) - .trim() - .replaceAll("\\s+", ""); - assertEquals(expectedXml, result.toString().trim().replaceAll("\\s+", "")); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - - // This ensures the test waits and fails if an exception occurs - future.join(); - } - - @Test - void executeCommandWithJsonElmType() { - ExecuteCommandParams params = new ExecuteCommandParams(); - params.setCommand("org.opencds.cqf.cql.ls.viewElm"); - params.setArguments(List.of( - JsonParser.parseString("\"\\/org\\/opencds\\/cqf\\/cql\\/ls\\/server\\/One.cql\""), - JsonParser.parseString("\"json\""))); - CompletableFuture future = viewElmCommandContribution - .executeCommand(params) - .thenAccept(result -> { - try { - String expectedJson = new String(Files.readAllBytes( - Paths.get("src/test/resources/org/opencds/cqf/cql/ls/server/One.json"))) - .trim() - .replaceAll("\\s+", ""); - assertEquals(expectedJson, result.toString().trim().replaceAll("\\s+", "")); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - - // This ensures the test waits and fails if an exception occurs - future.join(); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.java deleted file mode 100644 index 9697d762..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package org.opencds.cqf.cql.ls.server.manager; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.net.URI; -import java.util.Collections; -import java.util.List; -import org.cqframework.cql.cql2elm.CqlCompilerOptions; -import org.cqframework.cql.cql2elm.LibraryBuilder.SignatureLevel; -import org.eclipse.lsp4j.DidChangeWatchedFilesParams; -import org.eclipse.lsp4j.FileChangeType; -import org.eclipse.lsp4j.FileEvent; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.core.utility.Uris; -import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent; -import org.opencds.cqf.cql.ls.server.service.TestContentService; - -class CompilerOptionsManagerTest { - - private CompilerOptionsManager manager; - - // A URI whose "head" (parent path) is /org/opencds/cqf/cql/ls/server/ - // TestContentService.read will be called for the cql-options.json path, returning null - // (no such classpath resource), so default options are used. - private static final URI TEST_URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/One.cql"); - - @BeforeEach - void setUp() { - ContentService cs = new TestContentService(); - manager = new CompilerOptionsManager(cs); - } - - // ----------------------------------------------------------------------- - // getOptions — returns options enriched with required flags - // ----------------------------------------------------------------------- - - @Test - void getOptions_noOptionsFile_returnsNonNull() { - CqlCompilerOptions options = manager.getOptions(TEST_URI); - assertNotNull(options); - } - - @Test - void getOptions_alwaysIncludesEnableLocators() { - CqlCompilerOptions options = manager.getOptions(TEST_URI); - assertTrue(options.getOptions().contains(CqlCompilerOptions.Options.EnableLocators)); - } - - @Test - void getOptions_alwaysIncludesEnableResultTypes() { - CqlCompilerOptions options = manager.getOptions(TEST_URI); - assertTrue(options.getOptions().contains(CqlCompilerOptions.Options.EnableResultTypes)); - } - - @Test - void getOptions_alwaysIncludesEnableAnnotations() { - CqlCompilerOptions options = manager.getOptions(TEST_URI); - assertTrue(options.getOptions().contains(CqlCompilerOptions.Options.EnableAnnotations)); - } - - @Test - void getOptions_signatureLevelIsAll() { - CqlCompilerOptions options = manager.getOptions(TEST_URI); - assertEquals(SignatureLevel.All, options.getSignatureLevel()); - } - - // ----------------------------------------------------------------------- - // caching — same instance returned on second call - // ----------------------------------------------------------------------- - - @Test - void getOptions_secondCall_returnsCachedInstance() { - CqlCompilerOptions first = manager.getOptions(TEST_URI); - CqlCompilerOptions second = manager.getOptions(TEST_URI); - assertSame(first, second); - } - - // ----------------------------------------------------------------------- - // clearOptions — evicts cache so next call re-reads - // ----------------------------------------------------------------------- - - @Test - void clearOptions_evictsCache() { - manager.getOptions(TEST_URI); // populate cache - manager.clearOptions(TEST_URI); - CqlCompilerOptions second = manager.getOptions(TEST_URI); - // After clearing, a new instance is created and still satisfies the required-flags contract. - assertTrue(second.getOptions().contains(CqlCompilerOptions.Options.EnableLocators)); - } - - // ----------------------------------------------------------------------- - // onMessageEvent — cql-options.json change clears cache - // ----------------------------------------------------------------------- - - @Test - void onMessageEvent_cqlOptionsChanged_clearsCache() { - manager.getOptions(TEST_URI); // populate cache - - // Simulate a file-watch event for a cql-options.json under the same root. - String optionsUri = Uris.getHead(TEST_URI).toString() + "/cql/cql-options.json"; - FileEvent fileEvent = new FileEvent(optionsUri, FileChangeType.Changed); - DidChangeWatchedFilesParams params = new DidChangeWatchedFilesParams(List.of(fileEvent)); - manager.onMessageEvent(new DidChangeWatchedFilesEvent(params)); - - CqlCompilerOptions second = manager.getOptions(TEST_URI); - // After clearing via event, the options must still be enriched. - assertNotNull(second); - assertTrue(second.getOptions().contains(CqlCompilerOptions.Options.EnableLocators)); - } - - @Test - void onMessageEvent_unrelatedFile_doesNotClearCache() { - CqlCompilerOptions first = manager.getOptions(TEST_URI); // populate cache - - FileEvent fileEvent = new FileEvent("file:///workspace/SomeOtherFile.json", FileChangeType.Changed); - DidChangeWatchedFilesParams params = new DidChangeWatchedFilesParams(Collections.singletonList(fileEvent)); - manager.onMessageEvent(new DidChangeWatchedFilesEvent(params)); - - CqlCompilerOptions second = manager.getOptions(TEST_URI); - assertSame(first, second); // cache intact → same instance - } -} 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 deleted file mode 100644 index 612be057..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.opencds.cqf.cql.ls.server.provider; - -import static org.junit.jupiter.api.Assertions.*; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.URI; -import java.util.Set; -import org.hl7.elm.r1.VersionedIdentifier; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.core.utility.Uris; - -public class ContentServiceProviderTest { - - @Test - void should_throwException_when_gettingLibrary() throws Exception { - VersionedIdentifier versionedIdentifier = new VersionedIdentifier(); - versionedIdentifier.withVersion("1.0.0"); - - ContentServiceSourceProvider contentServiceSourceProvider = new ContentServiceSourceProvider( - Uris.parseOrNull("/provider/content/sample-library-1.0.0.json"), new ContentService() { - @Override - public Set locate(URI root, VersionedIdentifier identifier) { - throw new UncheckedIOException(new IOException()); - } - }); - - assertThrows(RuntimeException.class, () -> contentServiceSourceProvider.getLibrarySource(versionedIdentifier)); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProviderTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProviderTest.java deleted file mode 100644 index b4061771..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProviderTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.opencds.cqf.cql.ls.server.provider; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.net.URI; -import kotlinx.io.Source; -import org.cqframework.cql.cql2elm.LibrarySourceProvider; -import org.hl7.elm.r1.VersionedIdentifier; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.server.service.TestContentService; - -class ContentServiceSourceProviderTest { - - private static ContentServiceSourceProvider provider; - - @BeforeAll - static void beforeAll() { - ContentService cs = new TestContentService(); - // Root is arbitrary for TestContentService; it resolves by library name from classpath. - URI root = URI.create("file:///workspace/"); - provider = new ContentServiceSourceProvider(root, cs); - } - - @Test - void getLibrarySource_knownLibrary_returnsSource() { - // "One" resolves to /org/opencds/cqf/cql/ls/server/One.cql on classpath. - VersionedIdentifier id = new VersionedIdentifier().withId("One"); - - Source source = provider.getLibrarySource(id); - - assertNotNull(source); - } - - @Test - void getLibrarySource_unknownLibrary_returnsNull() { - VersionedIdentifier id = new VersionedIdentifier().withId("DoesNotExistLibrary"); - - Source source = provider.getLibrarySource(id); - - assertNull(source); - } - - @Test - void getLibrarySource_implementsLibrarySourceProvider() { - // Verify the class satisfies the LibrarySourceProvider contract expected by the compiler. - assertNotNull((LibrarySourceProvider) provider); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/FormattingProviderTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/FormattingProviderTest.java deleted file mode 100644 index bd23476e..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/FormattingProviderTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.opencds.cqf.cql.ls.server.provider; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.List; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.TextEdit; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.server.service.TestContentService; - -class FormattingProviderTest { - - private static FormattingProvider formattingProvider; - - @BeforeAll - static void beforeAll() { - ContentService cs = new TestContentService(); - formattingProvider = new FormattingProvider(cs); - } - - @Test - void format_validCql_returnsOneEdit() throws Exception { - List edits = formattingProvider.format("/org/opencds/cqf/cql/ls/server/Two.cql"); - assertEquals(1, edits.size()); - TextEdit edit = edits.get(0); - assertEquals(new Position(0, 0), edit.getRange().getStart()); - assertNotNull(edit.getNewText()); - assertFalse(edit.getNewText().isBlank()); - } - - @Test - void format_syntaxError_throwsIllegalArgument() { - assertThrows( - IllegalArgumentException.class, - () -> formattingProvider.format("/org/opencds/cqf/cql/ls/server/SyntaxError.cql")); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/HoverProviderTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/HoverProviderTest.java deleted file mode 100644 index dd04f544..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/provider/HoverProviderTest.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.opencds.cqf.cql.ls.server.provider; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import org.eclipse.lsp4j.Hover; -import org.eclipse.lsp4j.HoverParams; -import org.eclipse.lsp4j.MarkupContent; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.TextDocumentIdentifier; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.manager.IgContextManager; -import org.opencds.cqf.cql.ls.server.service.TestContentService; - -class HoverProviderTest { - - private static HoverProvider hoverProvider; - - @BeforeAll - static void beforeAll() { - ContentService cs = new TestContentService(); - CqlCompilationManager cqlCompilationManager = - new CqlCompilationManager(cs, new CompilerOptionsManager(cs), new IgContextManager(cs)); - hoverProvider = new HoverProvider(cqlCompilationManager); - } - - @Test - void hoverInt() throws Exception { - Hover hover = hoverProvider.hover(new HoverParams( - new TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), new Position(5, 2))); - - assertNotNull(hover); - assertNotNull(hover.getContents().getRight()); - - MarkupContent markup = hover.getContents().getRight(); - assertEquals("markdown", markup.getKind()); - assertEquals("```cql\nSystem.Integer\n```", markup.getValue()); - } - - @Test - void hoverNothing() throws Exception { - Hover hover = hoverProvider.hover(new HoverParams( - new TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), new Position(2, 0))); - - assertNull(hover); - } - - @Test - void hoverList() throws Exception { - Hover hover = hoverProvider.hover(new HoverParams( - new TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), new Position(8, 2))); - - assertNotNull(hover); - assertNotNull(hover.getContents().getRight()); - - MarkupContent markup = hover.getContents().getRight(); - assertEquals("markdown", markup.getKind()); - assertEquals("```cql\nlist\n```", markup.getValue()); - } - - @Test - void hoverOnLibraryRef() throws Exception { - // Line 5 (0-indexed): " 1 + One."One"" — position (5, 8) is 'O' in 'One' - Hover hover = hoverProvider.hover(new HoverParams( - new TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), new Position(5, 8))); - - assertNotNull(hover); - assertNotNull(hover.getContents().getRight()); - - MarkupContent markup = hover.getContents().getRight(); - assertEquals("markdown", markup.getKind()); - assertEquals("```cql\nSystem.Integer\n```", markup.getValue()); - } - - @Test - void hoverOnDefineName() throws Exception { - // Line 4 (0-indexed): "define "Two":" — position (4, 8) is 'T' inside the define name - Hover hover = hoverProvider.hover(new HoverParams( - new TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), new Position(4, 8))); - - // ExpressionDef TrackBack covers the define header — expect Integer type - assertNotNull(hover); - assertNotNull(hover.getContents().getRight()); - - MarkupContent markup = hover.getContents().getRight(); - assertEquals("markdown", markup.getKind()); - assertEquals("```cql\nSystem.Integer\n```", markup.getValue()); - } -} 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 deleted file mode 100644 index fab426ac..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/BadDataTest.java +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 7379058a..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CompartmentTest.java +++ /dev/null @@ -1,243 +0,0 @@ -package org.opencds.cqf.cql.ls.server.repository.ig.standard; - -import static org.junit.jupiter.api.Assertions.*; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.repository.IRepository; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; -import org.hl7.fhir.r4.model.*; -import org.junit.jupiter.api.*; -import org.junit.jupiter.api.io.TempDir; -import org.opencds.cqf.fhir.test.Resources; -import org.opencds.cqf.fhir.utility.Ids; -import org.opencds.cqf.fhir.utility.search.Searches; - -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class CompartmentTest { - - private static IRepository repository; - - @TempDir - static Path tempDir; - - @BeforeAll - static void setup() throws URISyntaxException, IOException, ClassNotFoundException { - // This copies the sample IG to a temporary directory so that - // we can test against an actual filesystem - Resources.copyFromJar("/sampleIgs/ig/standard/compartment", tempDir); - repository = new IgStandardRepository(FhirContext.forR4Cached(), tempDir); - } - - @Test - void readLibrary() { - var id = Ids.newId(Library.class, "123"); - var lib = repository.read(Library.class, id); - assertNotNull(lib); - assertEquals(id.getIdPart(), lib.getIdElement().getIdPart()); - } - - @Test - void readLibraryNotExists() { - var id = Ids.newId(Library.class, "DoesNotExist"); - assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); - } - - @Test - void searchLibrary() { - var libs = repository.search(Bundle.class, Library.class, Searches.ALL); - - assertNotNull(libs); - assertEquals(2, libs.getEntry().size()); - } - - @Test - void searchLibraryNotExists() { - var libs = repository.search(Bundle.class, Library.class, Searches.byUrl("not-exists")); - assertNotNull(libs); - assertEquals(0, libs.getEntry().size()); - } - - @Test - void readPatientNoCompartment() { - var id = Ids.newId(Patient.class, "123"); - assertThrows(ResourceNotFoundException.class, () -> repository.read(Patient.class, id)); - } - - @Test - void readPatient() { - var id = Ids.newId(Patient.class, "123"); - var p = repository.read(Patient.class, id, Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/123")); - - assertNotNull(p); - assertEquals(id.getIdPart(), p.getIdElement().getIdPart()); - } - - @Test - void searchEncounterNoCompartment() { - var encounters = repository.search(Bundle.class, Encounter.class, Searches.ALL); - assertNotNull(encounters); - assertEquals(0, encounters.getEntry().size()); - } - - @Test - void searchEncounter() { - var encounters = repository.search( - Bundle.class, - Encounter.class, - Searches.ALL, - Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/123")); - assertNotNull(encounters); - assertEquals(1, encounters.getEntry().size()); - } - - @Test - void readValueSetNoCompartment() { - var id = Ids.newId(ValueSet.class, "456"); - var vs = repository.read(ValueSet.class, id); - - assertNotNull(vs); - assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); - } - - // Terminology resources are not in compartments - @Test - void readValueSet() { - var id = Ids.newId(ValueSet.class, "456"); - var vs = repository.read( - ValueSet.class, id, Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/123")); - - assertNotNull(vs); - assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); - } - - @Test - void searchValueSet() { - var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/456")); - assertNotNull(sets); - assertEquals(1, sets.getEntry().size()); - } - - @Test - void createAndDeleteLibrary() { - var lib = new Library(); - lib.setId("new-library"); - var o = repository.create(lib); - var created = repository.read(Library.class, o.getId()); - assertNotNull(created); - - var loc = tempDir.resolve("resources/library/new-library.json"); - assertTrue(Files.exists(loc)); - - repository.delete(Library.class, created.getIdElement()); - assertFalse(Files.exists(loc)); - } - - @Test - void createAndDeletePatient() { - var p = new Patient(); - p.setId("new-patient"); - var header = Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/new-patient"); - var o = repository.create(p, header); - var created = repository.read(Patient.class, o.getId(), header); - assertNotNull(created); - - var loc = tempDir.resolve("tests/patient/new-patient/patient/new-patient.json"); - assertTrue(Files.exists(loc)); - - repository.delete(Patient.class, created.getIdElement(), header); - assertFalse(Files.exists(loc)); - } - - @Test - void createAndDeleteValueSet() { - var v = new ValueSet(); - v.setId("new-valueset"); - var o = repository.create(v); - var created = repository.read(ValueSet.class, o.getId()); - assertNotNull(created); - - var loc = tempDir.resolve("vocabulary/valueset/new-valueset.json"); - assertTrue(Files.exists(loc)); - - repository.delete(ValueSet.class, created.getIdElement()); - assertFalse(Files.exists(loc)); - } - - @Test - void updatePatient() { - var id = Ids.newId(Patient.class, "123"); - var p = repository.read(Patient.class, id, Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/123")); - assertFalse(p.hasActive()); - - p.setActive(true); - repository.update(p); - - var updated = - repository.read(Patient.class, id, Map.of(IgStandardRepository.FHIR_COMPARTMENT_HEADER, "Patient/123")); - assertTrue(updated.hasActive()); - assertTrue(updated.getActive()); - } - - @Test - void deleteNonExistentPatient() { - var id = Ids.newId(Patient.class, "DoesNotExist"); - assertThrows(ResourceNotFoundException.class, () -> repository.delete(Patient.class, id)); - } - - @Test - void searchNonExistentType() { - var results = repository.search(Bundle.class, Encounter.class, Searches.ALL); - assertNotNull(results); - assertEquals(0, results.getEntry().size()); - } - - @Test - void searchById() { - var bundle = repository.search(Bundle.class, Library.class, Searches.byId("123")); - assertNotNull(bundle); - assertEquals(1, bundle.getEntry().size()); - } - - @Test - void searchByIdNotFound() { - var bundle = repository.search(Bundle.class, Library.class, Searches.byId("DoesNotExist")); - assertNotNull(bundle); - assertEquals(0, bundle.getEntry().size()); - } - - @Test - @Order(1) // Do this test first because it puts the filesystem (temporarily) in an invalid state - void resourceMissingWhenCacheCleared() throws IOException { - var id = new IdType("Library", "ToDelete"); - var lib = new Library().setIdElement(id); - var path = tempDir.resolve("resources/library/ToDelete.json"); - - repository.create(lib); - assertTrue(path.toFile().exists()); - - // Read back, should exist - lib = repository.read(Library.class, id); - assertNotNull(lib); - - // Overwrite the file on disk. - Files.writeString(path, ""); - - // Read from cache, repo doesn't know the content is gone. - lib = repository.read(Library.class, id); - assertNotNull(lib); - assertEquals("ToDelete", lib.getIdElement().getIdPart()); - - ((IgStandardRepository) repository).clearCache(); - - // Try to read again, should be gone because it's not in the cache and the content is gone. - assertThrows(ResourceNotFoundException.class, () -> repository.read(Library.class, id)); - - // Clean up so that we don't affect other tests - path.toFile().delete(); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.java deleted file mode 100644 index aa6e14b3..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.opencds.cqf.cql.ls.server.repository.ig.standard; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Path; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.opencds.cqf.fhir.test.Resources; - -class ConventionsTest { - - @TempDir - static Path tempDir; - - @BeforeAll - static void setup() throws URISyntaxException, IOException, ClassNotFoundException { - // This copies the sample IG to a temporary directory so that - // we can test against an actual filesystem - Resources.copyFromJar("/sampleIgs/ig/standard", tempDir); - } - - @Test - void autoDetectDefault() { - assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(null)); - assertEquals( - IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("does_not_exist"))); - } - - @Test - void autoDetectStandard() { - assertEquals( - IgStandardConventions.STANDARD, - IgStandardConventions.autoDetect(tempDir.resolve("directoryPerType/standard"))); - } - - @Test - void autoDetectPrefix() { - var config = IgStandardConventions.autoDetect(tempDir.resolve("directoryPerType/prefixed")); - assertEquals(IgStandardConventions.FilenameMode.TYPE_AND_ID, config.filenameMode()); - assertEquals(IgStandardConventions.CategoryLayout.DIRECTORY_PER_CATEGORY, config.categoryLayout()); - assertEquals(IgStandardConventions.CompartmentLayout.FLAT, config.compartmentLayout()); - assertEquals(IgStandardConventions.FhirTypeLayout.DIRECTORY_PER_TYPE, config.typeLayout()); - } - - @Test - void autoDetectFlat() { - assertEquals(IgStandardConventions.FLAT, IgStandardConventions.autoDetect(tempDir.resolve("flat"))); - } - - @Test - void autoDetectFlatNoTypeNames() { - var config = IgStandardConventions.autoDetect(tempDir.resolve("flatNoTypeNames")); - assertEquals(IgStandardConventions.FilenameMode.ID_ONLY, config.filenameMode()); - assertEquals(IgStandardConventions.CategoryLayout.FLAT, config.categoryLayout()); - assertEquals(IgStandardConventions.CompartmentLayout.FLAT, config.compartmentLayout()); - assertEquals(IgStandardConventions.FhirTypeLayout.FLAT, config.typeLayout()); - } - - @Test - void autoDetectWithMisleadingFileName() { - assertEquals( - IgStandardConventions.STANDARD, - IgStandardConventions.autoDetect(tempDir.resolve("misleadingFileName"))); - } - - @Test - void autoDetectWithEmptyContent() { - assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("emptyContent"))); - } - - @Test - void autoDetectWithNonFhirFilename() { - assertEquals( - IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir.resolve("nonFhirFilename"))); - } - - @Test - void autoDetectWitCompartments() { - var config = IgStandardConventions.autoDetect(tempDir.resolve("compartment")); - assertEquals(IgStandardConventions.FilenameMode.ID_ONLY, config.filenameMode()); - assertEquals(IgStandardConventions.CategoryLayout.DIRECTORY_PER_CATEGORY, config.categoryLayout()); - assertEquals(IgStandardConventions.CompartmentLayout.DIRECTORY_PER_COMPARTMENT, config.compartmentLayout()); - assertEquals(IgStandardConventions.FhirTypeLayout.DIRECTORY_PER_TYPE, config.typeLayout()); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.java deleted file mode 100644 index 004af4a6..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.opencds.cqf.cql.ls.server.repository.ig.standard; - -import static org.junit.jupiter.api.Assertions.*; - -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Path; -import org.hl7.fhir.dstu2.model.ValueSet; -import org.hl7.fhir.dstu3.model.Library; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.opencds.cqf.fhir.test.Resources; - -class CqlContentTest { - - @TempDir - static Path tempDir; - - @BeforeAll - static void setup() throws ClassNotFoundException, URISyntaxException, IOException { - // This copies the sample IG to a temporary directory so that - // we can test against an actual filesystem - Resources.copyFromJar("/sampleIgs/ig/standard/cqlContent", tempDir); - } - - @Test - void loadCqlContentDstu3() { - var lib = new Library(); - lib.addContent().setContentType("text/cql").setUrl("cql/Test.cql"); - IgStandardCqlContent.loadCqlContent(lib, tempDir); - assertNotNull(lib.getContentFirstRep().getData()); - } - - @Test - void loadCqlContentR4() { - var lib = new org.hl7.fhir.r4.model.Library(); - lib.addContent().setContentType("text/cql").setUrl("cql/Test.cql"); - IgStandardCqlContent.loadCqlContent(lib, tempDir); - assertNotNull(lib.getContentFirstRep().getData()); - } - - @Test - void loadCqlContentR5() { - var lib = new org.hl7.fhir.r5.model.Library(); - lib.addContent().setContentType("text/cql").setUrl("cql/Test.cql"); - IgStandardCqlContent.loadCqlContent(lib, tempDir); - assertNotNull(lib.getContentFirstRep().getData()); - } - - @Test - void emptyLibraryDoesNothing() { - var lib = new Library(); - IgStandardCqlContent.loadCqlContent(lib, tempDir); - assertEquals(0, lib.getContent().size()); - } - - @Test - void nonLibraryResourceDoesNotThrow() { - assertDoesNotThrow(() -> IgStandardCqlContent.loadCqlContent(new ValueSet(), tempDir)); - } - - @Test - void invalidFhirVersionThrows() { - var lib = new org.hl7.fhir.r4b.model.Library(); - assertThrows(IllegalArgumentException.class, () -> IgStandardCqlContent.loadCqlContent(lib, tempDir)); - } - - @Test - void invalidPathThrows() { - var lib = new org.hl7.fhir.r4.model.Library(); - lib.addContent().setContentType("text/cql").setUrl("not-a-real-path/Test.cql"); - assertThrows(ResourceNotFoundException.class, () -> IgStandardCqlContent.loadCqlContent(lib, tempDir)); - } - - @Test - void nullThrows() { - assertThrows(NullPointerException.class, () -> IgStandardCqlContent.loadCqlContent(null, tempDir)); - - var lib = new Library(); - assertThrows(NullPointerException.class, () -> IgStandardCqlContent.loadCqlContent(lib, null)); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.java deleted file mode 100644 index 443d0ba1..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.java +++ /dev/null @@ -1,176 +0,0 @@ -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 deleted file mode 100644 index 39d9da8a..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/ExternalTest.java +++ /dev/null @@ -1,87 +0,0 @@ -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 deleted file mode 100644 index 42c5289e..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatNoTypeNamesTest.java +++ /dev/null @@ -1,176 +0,0 @@ -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 deleted file mode 100644 index eb7d0b85..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatTest.java +++ /dev/null @@ -1,176 +0,0 @@ -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 deleted file mode 100644 index fd6be6d5..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MixedEncodingTest.java +++ /dev/null @@ -1,181 +0,0 @@ -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 deleted file mode 100644 index e1c5fdc7..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.opencds.cqf.cql.ls.server.repository.ig.standard; - -import static org.junit.jupiter.api.Assertions.*; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.repository.IRepository; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.hl7.fhir.r4.model.Library; -import org.hl7.fhir.r4.model.Patient; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.opencds.cqf.fhir.test.Resources; -import org.opencds.cqf.fhir.utility.Ids; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MultiMeasureTest { - private static final Logger log = LoggerFactory.getLogger(MultiMeasureTest.class); - - private static final String rootDir = "/sampleIgs/ig/standard/cqlMeasures/multiMeasure"; - private static final String modelPathMeasure100TestCase1111 = "input/tests/measure/measure100/1111"; - private static final String modelPathMeasure100TestCase2222 = "input/tests/measure/measure100/2222"; - private static final String modelPathMeasure200TestCase1111 = "input/tests/measure/measure200/1111"; - private static final String terminologyPath = "input/vocabulary/valueset"; - - @TempDir - static Path tempDir; - - @TempDir - static Path pathModelPathMeasure100TestCase1111; - - @TempDir - static Path pathModelPathMeasure100TestCase2222; - - @TempDir - static Path pathModelPathMeasure200TestCase1111; - - @TempDir - static Path pathTerminology; - - static IRepository model1111Measure100Repo; - static IRepository model2222Measure100Repo; - static IRepository model1111Measure200Repo; - static IRepository terminologyRepo; - - static void listFiles(Path path) { - var pathExists = path.toFile().exists(); - log.info("path[{}] exists: {}", path, pathExists); - if (pathExists) - try (Stream stream = Files.walk(path)) { - String fileNames = stream.map(Path::toString).collect(Collectors.joining("\n ")); - - log.info("resources: \n {}", fileNames); - } catch (IOException e) { - log.error("Exception while capturing filenames. {}", e.getMessage()); - } - } - - @BeforeAll - static void setup() throws URISyntaxException, IOException, ClassNotFoundException { - // This copies the sample IG to a temporary directory so that - // we can test against an actual filesystem - Resources.copyFromJar(rootDir, tempDir); - - pathModelPathMeasure100TestCase1111 = tempDir.resolve(modelPathMeasure100TestCase1111); - pathModelPathMeasure100TestCase2222 = tempDir.resolve(modelPathMeasure100TestCase2222); - pathModelPathMeasure200TestCase1111 = tempDir.resolve(modelPathMeasure200TestCase1111); - pathTerminology = tempDir.resolve(terminologyPath); - - model1111Measure100Repo = - new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure100TestCase1111); - model2222Measure100Repo = - new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure100TestCase2222); - model1111Measure200Repo = - new IgStandardRepository(FhirContext.forR4Cached(), pathModelPathMeasure200TestCase1111); - terminologyRepo = new IgStandardRepository(FhirContext.forR4Cached(), pathTerminology); - - listFiles(tempDir); - listFiles(pathModelPathMeasure100TestCase1111); - listFiles(pathModelPathMeasure100TestCase2222); - listFiles(pathModelPathMeasure200TestCase1111); - listFiles(pathTerminology); - } - - @Test - void should_throwException_when_libraryDoesNotExist() { - var id = Ids.newId(Library.class, "DoesNotExist"); - assertThrows(ResourceNotFoundException.class, () -> model1111Measure100Repo.read(Library.class, id)); - assertThrows(ResourceNotFoundException.class, () -> model2222Measure100Repo.read(Library.class, id)); - assertThrows(ResourceNotFoundException.class, () -> model1111Measure200Repo.read(Library.class, id)); - assertThrows(ResourceNotFoundException.class, () -> terminologyRepo.read(Library.class, id)); - } - - // Test works locally but doesn't work on GitHub - // @Disabled("Disabled until issue with running test on github is resolved.") - @Test - void should_findResourceInCorrectRepo_when_resourcesIsolatedByRepo() { - var id = Ids.newId(Patient.class, "1111"); - var patientFrommModel1111Measure100Repo = model1111Measure100Repo.read(Patient.class, id); - var patientFrommModel1111Measure200Repo = model1111Measure200Repo.read(Patient.class, id); - - assertEquals( - id.getIdPart(), - patientFrommModel1111Measure100Repo.getIdElement().getIdPart()); - assertEquals( - id.getIdPart(), - patientFrommModel1111Measure200Repo.getIdElement().getIdPart()); - assertNotEquals(patientFrommModel1111Measure100Repo.getName(), patientFrommModel1111Measure200Repo.getName()); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.java deleted file mode 100644 index 39e4e96d..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.java +++ /dev/null @@ -1,141 +0,0 @@ -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 deleted file mode 100644 index a432871f..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/OverwriteEncodingTest.java +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index a2f6d9b2..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/PrefixTest.java +++ /dev/null @@ -1,221 +0,0 @@ -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 deleted file mode 100644 index 7821c83d..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/XmlWriteTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.opencds.cqf.cql.ls.server.repository.ig.standard; - -import static org.junit.jupiter.api.Assertions.*; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.repository.IRepository; -import ca.uhn.fhir.rest.api.EncodingEnum; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Library; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.ValueSet; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.opencds.cqf.fhir.test.Resources; -import org.opencds.cqf.fhir.utility.Ids; -import org.opencds.cqf.fhir.utility.search.Searches; - -class XmlWriteTest { - - private static IRepository repository; - - @TempDir - static Path tempDir; - - @BeforeAll - static void setup() throws URISyntaxException, IOException, ClassNotFoundException { - // This copies the sample IG to a temporary directory so that - // we can test against an actual filesystem - Resources.copyFromJar("/sampleIgs/ig/standard/mixedEncoding", tempDir); - var conventions = IgStandardConventions.autoDetect(tempDir); - repository = new IgStandardRepository( - FhirContext.forR4Cached(), - tempDir, - conventions, - new IgStandardEncodingBehavior( - EncodingEnum.XML, IgStandardEncodingBehavior.PreserveEncoding.PRESERVE_ORIGINAL_ENCODING), - null); - } - - @Test - void createAndDeleteLibrary() { - var lib = new Library(); - lib.setId("new-library"); - var o = repository.create(lib); - var created = repository.read(Library.class, o.getId()); - assertNotNull(created); - - var loc = tempDir.resolve("resources/library/new-library.xml"); - assertTrue(Files.exists(loc)); - - repository.delete(Library.class, created.getIdElement()); - assertFalse(Files.exists(loc)); - } - - @Test - void createAndDeletePatient() { - var p = new Patient(); - p.setId("new-patient"); - var o = repository.create(p); - var created = repository.read(Patient.class, o.getId()); - assertNotNull(created); - - var loc = tempDir.resolve("tests/patient/new-patient.xml"); - assertTrue(Files.exists(loc)); - - repository.delete(Patient.class, created.getIdElement()); - assertFalse(Files.exists(loc)); - } - - @Test - void readExternalValueSet() { - var id = Ids.newId(ValueSet.class, "789"); - var vs = repository.read(ValueSet.class, id); - assertNotNull(vs); - assertEquals(vs.getIdPart(), vs.getIdElement().getIdPart()); - - // Should be tagged with its source path - var path = (Path) vs.getUserData(IgStandardRepository.SOURCE_PATH_TAG); - assertNotNull(path); - assertTrue(path.toFile().exists()); - assertTrue(path.toString().contains("external")); - } - - @Test - void searchExternalValueSet() { - var sets = repository.search(Bundle.class, ValueSet.class, Searches.byUrl("example.com/ValueSet/789")); - assertNotNull(sets); - assertEquals(1, sets.getEntry().size()); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.java deleted file mode 100644 index 83197187..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.java +++ /dev/null @@ -1,188 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.net.URI; -import java.util.Collections; -import java.util.Set; -import org.eclipse.lsp4j.DidChangeTextDocumentParams; -import org.eclipse.lsp4j.DidCloseTextDocumentParams; -import org.eclipse.lsp4j.DidOpenTextDocumentParams; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.TextDocumentContentChangeEvent; -import org.eclipse.lsp4j.TextDocumentIdentifier; -import org.eclipse.lsp4j.TextDocumentItem; -import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; -import org.hl7.elm.r1.VersionedIdentifier; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.cql.ls.core.utility.Uris; -import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent; -import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent; - -class ActiveContentServiceTest { - - private static final String DOC_URI = "file:///workspace/One.cql"; - private static final URI DOC_URI_PARSED = Uris.parseOrNull(DOC_URI); - private static final URI ROOT = Uris.parseOrNull("file:///workspace/"); - - private static final String LIBRARY_CONTENT = "library One version '1.0.0'\n\ndefine \"Test\":\n 1\n"; - - private ActiveContentService openDoc(String text, int version) throws Exception { - ActiveContentService svc = new ActiveContentService(); - DidOpenTextDocumentParams params = new DidOpenTextDocumentParams(); - TextDocumentItem item = new TextDocumentItem(); - item.setUri(DOC_URI); - item.setText(text); - item.setVersion(version); - params.setTextDocument(item); - svc.didOpen(new DidOpenTextDocumentEvent(params)); - return svc; - } - - // ----------------------------------------------------------------------- - // didOpen / didClose - // ----------------------------------------------------------------------- - - @Test - void didOpen_addsUriToActiveSet() throws Exception { - ActiveContentService svc = openDoc(LIBRARY_CONTENT, 1); - assertTrue(svc.activeUris().contains(DOC_URI_PARSED)); - } - - @Test - void didClose_removesUriFromActiveSet() throws Exception { - ActiveContentService svc = openDoc(LIBRARY_CONTENT, 1); - - DidCloseTextDocumentParams close = new DidCloseTextDocumentParams(); - TextDocumentIdentifier docId = new TextDocumentIdentifier(DOC_URI); - close.setTextDocument(docId); - svc.didClose(new DidCloseTextDocumentEvent(close)); - - assertFalse(svc.activeUris().contains(DOC_URI_PARSED)); - } - - // ----------------------------------------------------------------------- - // didChange - // ----------------------------------------------------------------------- - - @Test - void didChange_fullReplacement_updatesContent() throws Exception { - ActiveContentService svc = openDoc("old content", 1); - - DidChangeTextDocumentParams change = new DidChangeTextDocumentParams(); - VersionedTextDocumentIdentifier vid = new VersionedTextDocumentIdentifier(DOC_URI, 2); - change.setTextDocument(vid); - TextDocumentContentChangeEvent ev = new TextDocumentContentChangeEvent("new content"); - change.setContentChanges(Collections.singletonList(ev)); - - svc.didChange(new DidChangeTextDocumentEvent(change)); - - // Read the updated content back - String updated = new String(svc.read(DOC_URI_PARSED).readAllBytes()); - assertEquals("new content", updated); - } - - @Test - void didChange_olderVersion_contentUnchanged() throws Exception { - ActiveContentService svc = openDoc("original", 5); - - DidChangeTextDocumentParams change = new DidChangeTextDocumentParams(); - // version 3 < current version 5, so change should be ignored - VersionedTextDocumentIdentifier vid = new VersionedTextDocumentIdentifier(DOC_URI, 3); - change.setTextDocument(vid); - TextDocumentContentChangeEvent ev = new TextDocumentContentChangeEvent("should be ignored"); - change.setContentChanges(Collections.singletonList(ev)); - - svc.didChange(new DidChangeTextDocumentEvent(change)); - - String content = new String(svc.read(DOC_URI_PARSED).readAllBytes()); - assertEquals("original", content); - } - - // ----------------------------------------------------------------------- - // patch - // ----------------------------------------------------------------------- - - @Test - @SuppressWarnings("deprecation") - void patch_singleLineReplacement_replacesCorrectRange() throws Exception { - ActiveContentService svc = new ActiveContentService(); - - TextDocumentContentChangeEvent change = new TextDocumentContentChangeEvent(); - change.setRange(new Range(new Position(0, 6), new Position(0, 11))); - change.setText("CQL"); - change.setRangeLength(5); - - String result = svc.patch("hello world", change); - - assertEquals("hello CQL", result); - } - - @Test - @SuppressWarnings("deprecation") - void patch_multiLineReplacement_replacesAcrossLines() throws Exception { - ActiveContentService svc = new ActiveContentService(); - - // Source: "line1\nline2\nline3" - // Replace from (0,5) to end of "line2": replacement = " and " - // rangeLength = 6 chars ("\nline2") - TextDocumentContentChangeEvent change = new TextDocumentContentChangeEvent(); - change.setRange(new Range(new Position(0, 5), new Position(1, 5))); - change.setText(" and "); - change.setRangeLength(6); - - String result = svc.patch("line1\nline2\nline3", change); - - assertEquals("line1 and \nline3", result); - } - - // ----------------------------------------------------------------------- - // searchActiveContent - // ----------------------------------------------------------------------- - - @Test - void searchActiveContent_matchesNameAndVersion() throws Exception { - ActiveContentService svc = openDoc(LIBRARY_CONTENT, 1); - - VersionedIdentifier id = new VersionedIdentifier().withId("One").withVersion("1.0.0"); - Set found = svc.searchActiveContent(ROOT, id); - - assertTrue(found.contains(DOC_URI_PARSED)); - } - - @Test - void searchActiveContent_uriOutsideRoot_excluded() throws Exception { - // The document URI is under /other/, not under ROOT (/workspace/). - String otherUri = "file:///other/One.cql"; - ActiveContentService svc = new ActiveContentService(); - DidOpenTextDocumentParams params = new DidOpenTextDocumentParams(); - TextDocumentItem item = new TextDocumentItem(); - item.setUri(otherUri); - item.setText(LIBRARY_CONTENT); - item.setVersion(1); - params.setTextDocument(item); - svc.didOpen(new DidOpenTextDocumentEvent(params)); - - VersionedIdentifier id = new VersionedIdentifier().withId("One").withVersion("1.0.0"); - Set found = svc.searchActiveContent(ROOT, id); - - assertFalse(found.contains(Uris.parseOrNull(otherUri))); - } - - @Test - void searchActiveContent_contentDoesNotMatch_notIncluded() throws Exception { - ActiveContentService svc = openDoc("library Two version '2.0.0'\n\ndefine \"X\": 1", 1); - - // Searching for One 1.0.0 when doc contains Two 2.0.0 → no match - VersionedIdentifier id = new VersionedIdentifier().withId("One").withVersion("1.0.0"); - Set found = svc.searchActiveContent(ROOT, id); - - assertNotNull(found); - assertFalse(found.contains(DOC_URI_PARSED)); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.java deleted file mode 100644 index f79573d7..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.net.URI; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.DiagnosticSeverity; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.services.LanguageClient; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.core.utility.Uris; -import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.manager.IgContextManager; - -class DiagnosticsServiceTest { - - private static DiagnosticsService diagnosticsService; - - @BeforeAll - static void beforeAll() { - ContentService cs = new TestContentService(); - CqlCompilationManager cqlCompilationManager = - new CqlCompilationManager(cs, new CompilerOptionsManager(cs), new IgContextManager(cs)); - diagnosticsService = new DiagnosticsService( - CompletableFuture.completedFuture(Mockito.mock(LanguageClient.class)), cqlCompilationManager, cs); - } - - @Test - void missingInclude() { - URI uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/MissingInclude.cql"); - Map> diagnostics = diagnosticsService.lint(uri); - - assertTrue(diagnostics.containsKey(uri)); - - Set dSet = diagnostics.get(uri); - - assertEquals(1, dSet.size()); - - Diagnostic d = dSet.iterator().next(); - - assertEquals(d.getRange(), new Range(new Position(2, 0), new Position(2, 15))); - } - - @Test - void validCql_noErrors() { - URI uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/Two.cql"); - Map> diagnostics = diagnosticsService.lint(uri); - - assertTrue(diagnostics.containsKey(uri)); - assertTrue(diagnostics.get(uri).isEmpty()); - } - - @Test - void syntaxError_returnsDiagnostic() { - URI uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/SyntaxError.cql"); - Map> diagnostics = diagnosticsService.lint(uri); - - assertTrue(diagnostics.containsKey(uri)); - assertFalse(diagnostics.get(uri).isEmpty()); - - Diagnostic d = diagnostics.get(uri).iterator().next(); - assertEquals(DiagnosticSeverity.Error, d.getSeverity()); - } - - @Test - void oneCqlValid_noErrors() { - // One.cql is a simple valid library with no includes; lint should produce no diagnostics. - URI uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/One.cql"); - Map> diagnostics = diagnosticsService.lint(uri); - - assertTrue(diagnostics.containsKey(uri)); - assertTrue(diagnostics.get(uri).isEmpty()); - } - - @Test - void missingInclude_diagnosticHasNonNullMessage() { - URI uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/MissingInclude.cql"); - Map> diagnostics = diagnosticsService.lint(uri); - - Set dSet = diagnostics.get(uri); - assertFalse(dSet.isEmpty()); - - Diagnostic d = dSet.iterator().next(); - assertNotNull(d.getMessage()); - assertFalse(d.getMessage().isEmpty()); - } - - @Test - void missingInclude_diagnosticHasNonNullRange() { - URI uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/MissingInclude.cql"); - Map> diagnostics = diagnosticsService.lint(uri); - - Diagnostic d = diagnostics.get(uri).iterator().next(); - assertNotNull(d.getRange()); - assertNotNull(d.getRange().getStart()); - assertNotNull(d.getRange().getEnd()); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.kt b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.kt new file mode 100644 index 00000000..7adae6e8 --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.kt @@ -0,0 +1,108 @@ +package org.opencds.cqf.cql.ls.server.service + +import org.eclipse.lsp4j.Diagnostic +import org.eclipse.lsp4j.DiagnosticSeverity +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.services.LanguageClient +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import java.net.URI +import java.util.concurrent.CompletableFuture + +class DiagnosticsServiceTest { + + companion object { + private lateinit var diagnosticsService: DiagnosticsService + + @BeforeAll + @JvmStatic + fun beforeAll() { + val cs = TestContentService() + val compilationManager = CqlCompilationManager(cs, CompilerOptionsManager(cs), IgContextManager(cs)) + diagnosticsService = DiagnosticsService( + CompletableFuture.completedFuture(Mockito.mock(LanguageClient::class.java)), + compilationManager, + cs + ) + } + } + + @Test + fun missingInclude() { + val uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/MissingInclude.cql")!! + val diagnostics = diagnosticsService.lint(uri) + + assertTrue(diagnostics.containsKey(uri)) + + val dSet = diagnostics[uri]!! + assertEquals(1, dSet.size) + + val d = dSet.iterator().next() + assertEquals(d.range, Range(Position(2, 0), Position(2, 15))) + } + + @Test + fun validCql_noErrors() { + val uri: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/Two.cql")!! + val diagnostics = diagnosticsService.lint(uri) + + assertTrue(diagnostics.containsKey(uri)) + assertTrue(diagnostics[uri]!!.isEmpty()) + } + + @Test + fun syntaxError_returnsDiagnostic() { + val uri: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/SyntaxError.cql")!! + val diagnostics = diagnosticsService.lint(uri) + + assertTrue(diagnostics.containsKey(uri)) + assertFalse(diagnostics[uri]!!.isEmpty()) + + val d: Diagnostic = diagnostics[uri]!!.iterator().next() + assertEquals(DiagnosticSeverity.Error, d.severity) + } + + @Test + fun oneCqlValid_noErrors() { + // One.cql is a simple valid library with no includes; lint should produce no diagnostics. + val uri: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/One.cql")!! + val diagnostics = diagnosticsService.lint(uri) + + assertTrue(diagnostics.containsKey(uri)) + assertTrue(diagnostics[uri]!!.isEmpty()) + } + + @Test + fun missingInclude_diagnosticHasNonNullMessage() { + val uri: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/MissingInclude.cql")!! + val diagnostics = diagnosticsService.lint(uri) + + val dSet = diagnostics[uri]!! + assertFalse(dSet.isEmpty()) + + val d: Diagnostic = dSet.iterator().next() + assertNotNull(d.message) + assertFalse(d.message.isEmpty()) + } + + @Test + fun missingInclude_diagnosticHasNonNullRange() { + val uri: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/MissingInclude.cql")!! + val diagnostics = diagnosticsService.lint(uri) + + val d: Diagnostic = diagnostics[uri]!!.iterator().next() + assertNotNull(d.range) + assertNotNull(d.range.start) + assertNotNull(d.range.end) + } +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.java deleted file mode 100644 index 0cbb72b2..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.net.URI; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import org.hl7.elm.r1.VersionedIdentifier; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.cql.ls.core.ContentService; - -class FederatedContentServiceTest { - - private ActiveContentService activeService; - private ContentService fileService; - private FederatedContentService fedService; - - private static final URI ROOT = URI.create("file:///workspace/"); - private static final URI ACTIVE_URI = URI.create("file:///workspace/One.cql"); - private static final URI FILE_URI = URI.create("file:///workspace/lib/One.cql"); - - @BeforeEach - void setUp() { - activeService = mock(ActiveContentService.class); - fileService = mock(ContentService.class); - fedService = new FederatedContentService(activeService, fileService); - } - - // ----------------------------------------------------------------------- - // locate — merges results from both services - // ----------------------------------------------------------------------- - - @Test - void locate_mergesActiveAndFileResults() { - VersionedIdentifier id = new VersionedIdentifier().withId("One").withVersion("1.0.0"); - Set activeResult = new HashSet<>(Collections.singletonList(ACTIVE_URI)); - Set fileResult = new HashSet<>(Collections.singletonList(FILE_URI)); - - when(activeService.locate(ROOT, id)).thenReturn(activeResult); - when(fileService.locate(ROOT, id)).thenReturn(fileResult); - - Set result = fedService.locate(ROOT, id); - - assertTrue(result.contains(ACTIVE_URI)); - assertTrue(result.contains(FILE_URI)); - } - - @Test - void locate_emptyFromBoth_returnsEmptySet() { - VersionedIdentifier id = new VersionedIdentifier().withId("Unknown"); - when(activeService.locate(ROOT, id)).thenReturn(new HashSet<>()); - when(fileService.locate(ROOT, id)).thenReturn(new HashSet<>()); - - Set result = fedService.locate(ROOT, id); - - assertTrue(result.isEmpty()); - } - - // ----------------------------------------------------------------------- - // read — prefers active content when URI is in active set - // ----------------------------------------------------------------------- - - @Test - void read_uriInActiveSet_returnsActiveStream() { - InputStream expected = new ByteArrayInputStream("active content".getBytes()); - when(activeService.activeUris()).thenReturn(Collections.singleton(ACTIVE_URI)); - when(activeService.read(ACTIVE_URI)).thenReturn(expected); - - InputStream result = fedService.read(ACTIVE_URI); - - assertSame(expected, result); - } - - @Test - void read_uriNotInActiveSet_fallsBackToFileService() { - InputStream expected = new ByteArrayInputStream("file content".getBytes()); - when(activeService.activeUris()).thenReturn(Collections.emptySet()); - when(fileService.read(FILE_URI)).thenReturn(expected); - - InputStream result = fedService.read(FILE_URI); - - assertSame(expected, result); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/FileContentServiceTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/FileContentServiceTest.java deleted file mode 100644 index d0528623..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/FileContentServiceTest.java +++ /dev/null @@ -1,143 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.File; -import java.io.InputStream; -import java.net.URI; -import java.util.Collections; -import java.util.Set; -import org.eclipse.lsp4j.WorkspaceFolder; -import org.hl7.elm.r1.VersionedIdentifier; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class FileContentServiceTest { - - @TempDir - File tempDir; - - private VersionedIdentifier id(String name, String version) { - return new VersionedIdentifier().withId(name).withVersion(version); - } - - private VersionedIdentifier id(String name) { - return new VersionedIdentifier().withId(name); - } - - // ----------------------------------------------------------------------- - // searchFolder (static) - // ----------------------------------------------------------------------- - - @Test - void searchFolder_exactVersionedMatch_returnsFile() throws Exception { - File cqlFile = new File(tempDir, "FHIRHelpers-4.0.1.cql"); - cqlFile.createNewFile(); - - File result = FileContentService.searchFolder(tempDir.toURI(), id("FHIRHelpers", "4.0.1")); - - assertNotNull(result); - assertEquals(cqlFile.getCanonicalPath(), result.getCanonicalPath()); - } - - @Test - void searchFolder_unversionedFilename_treatedAsNearestMatch() throws Exception { - // A file named "FHIRHelpers.cql" (no version suffix) is returned when - // the versioned file does not exist, as it is treated as the most-recent version. - File cqlFile = new File(tempDir, "FHIRHelpers.cql"); - cqlFile.createNewFile(); - - File result = FileContentService.searchFolder(tempDir.toURI(), id("FHIRHelpers", "4.0.1")); - - assertNotNull(result); - assertEquals(cqlFile.getCanonicalPath(), result.getCanonicalPath()); - } - - @Test - void searchFolder_noMatch_returnsNull() { - File result = FileContentService.searchFolder(tempDir.toURI(), id("NonExistent", "1.0.0")); - assertNull(result); - } - - @Test - void searchFolder_compatibleVersion_returnsNearest() throws Exception { - // 4.0.1 is compatible with a request for 4.0.0 (same major/minor, higher patch). - new File(tempDir, "FHIRHelpers-4.0.1.cql").createNewFile(); - - File result = FileContentService.searchFolder(tempDir.toURI(), id("FHIRHelpers", "4.0.0")); - - assertNotNull(result); - } - - @Test - void searchFolder_noMatchingPrefix_returnsNull() throws Exception { - // "SomeOtherLib.cql" does not start with "MyLib", so the filter never returns it. - new File(tempDir, "SomeOtherLib.cql").createNewFile(); - - File result = FileContentService.searchFolder(tempDir.toURI(), id("MyLib", "1.0.0")); - - assertNull(result); - } - - // ----------------------------------------------------------------------- - // read(URI) - // ----------------------------------------------------------------------- - - @Test - void read_existingFile_returnsStream() throws Exception { - File cqlFile = new File(tempDir, "Test.cql"); - cqlFile.createNewFile(); - - FileContentService svc = new FileContentService(Collections.emptyList()); - InputStream stream = svc.read(cqlFile.toURI()); - - assertNotNull(stream); - stream.close(); - } - - @Test - void read_nonExistentFile_returnsNull() { - FileContentService svc = new FileContentService(Collections.emptyList()); - URI uri = new File(tempDir, "does-not-exist.cql").toURI(); - - assertNull(svc.read(uri)); - } - - // ----------------------------------------------------------------------- - // locate(URI, VersionedIdentifier) - // ----------------------------------------------------------------------- - - @Test - void locate_rootWithinWorkspace_returnsUri() throws Exception { - new File(tempDir, "One.cql").createNewFile(); - - WorkspaceFolder folder = new WorkspaceFolder(); - folder.setUri(tempDir.toURI().toString()); - FileContentService svc = new FileContentService(Collections.singletonList(folder)); - - Set result = svc.locate(tempDir.toURI(), id("One")); - - assertNotNull(result); - assertTrue(result.size() >= 1); - } - - @Test - void locate_rootOutsideWorkspace_returnsEmpty() throws Exception { - File workspace = new File(tempDir, "workspace"); - workspace.mkdirs(); - new File(workspace, "One.cql").createNewFile(); - - // workspace folder is the subdirectory; root is the parent tempDir - WorkspaceFolder folder = new WorkspaceFolder(); - folder.setUri(workspace.toURI().toString()); - FileContentService svc = new FileContentService(Collections.singletonList(folder)); - - // root = tempDir, which is NOT inside the workspace subfolder - Set result = svc.locate(tempDir.toURI(), id("One")); - - assertTrue(result.isEmpty()); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/TestContentService.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/TestContentService.java deleted file mode 100644 index 593b4ced..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/TestContentService.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.opencds.cqf.cql.ls.server.service; - -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collections; -import java.util.Set; -import org.hl7.elm.r1.VersionedIdentifier; -import org.opencds.cqf.cql.ls.core.ContentService; - -public class TestContentService implements ContentService { - - @Override - public Set locate(URI root, VersionedIdentifier libraryIdentifier) { - try { - return Collections.singleton( - new URI("/org/opencds/cqf/cql/ls/server/" + libraryIdentifier.getId() + ".cql")); - } catch (URISyntaxException e) { - throw new RuntimeException( - String.format("error locating test contest for: %s", libraryIdentifier.toString())); - } - } - - public InputStream read(URI uri) { - return TestContentService.class.getResourceAsStream(uri.toString()); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/TestContentService.kt b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/TestContentService.kt new file mode 100644 index 00000000..d1929c7b --- /dev/null +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/TestContentService.kt @@ -0,0 +1,15 @@ +package org.opencds.cqf.cql.ls.server.service + +import org.hl7.elm.r1.VersionedIdentifier +import org.opencds.cqf.cql.ls.core.ContentService +import java.io.InputStream +import java.net.URI + +class TestContentService : ContentService { + + override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = + setOf(URI("/org/opencds/cqf/cql/ls/server/${libraryIdentifier.id}.cql")) + + override fun read(uri: URI): InputStream? = + TestContentService::class.java.getResourceAsStream(uri.toString()) +} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/utility/DiagnosticsTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/utility/DiagnosticsTest.java deleted file mode 100644 index baad01b2..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/utility/DiagnosticsTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.opencds.cqf.cql.ls.server.utility; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Set; -import org.cqframework.cql.cql2elm.CqlCompilerException; -import org.cqframework.cql.cql2elm.CqlSemanticException; -import org.cqframework.cql.cql2elm.tracking.TrackBack; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.DiagnosticSeverity; -import org.hl7.elm.r1.VersionedIdentifier; -import org.junit.jupiter.api.Test; - -class DiagnosticsTest { - - private static TrackBack locator(int startLine, int startChar, int endLine, int endChar) { - return new TrackBack(new VersionedIdentifier(), startLine, startChar, endLine, endChar); - } - - private static CqlSemanticException exception( - String message, TrackBack tb, CqlCompilerException.ErrorSeverity severity) { - return new CqlSemanticException(message, tb, severity, null); - } - - // ----------------------------------------------------------------------- - // convert(CqlCompilerException) - // ----------------------------------------------------------------------- - - @Test - void convert_withLocator_appliesIndexConversion() { - // TrackBack is 1-indexed; LSP is 0-indexed. - // startChar -1, endChar no -1. - TrackBack tb = locator(5, 3, 5, 15); - Diagnostic d = Diagnostics.convert(exception("err", tb, CqlCompilerException.ErrorSeverity.Error)); - - assertNotNull(d); - assertEquals(4, d.getRange().getStart().getLine()); - assertEquals(2, d.getRange().getStart().getCharacter()); - assertEquals(4, d.getRange().getEnd().getLine()); - assertEquals(15, d.getRange().getEnd().getCharacter()); // end char: no -1 - } - - @Test - void convert_withNullLocator_returnsNull() { - CqlSemanticException error = - new CqlSemanticException("no locator", null, CqlCompilerException.ErrorSeverity.Error, null); - assertNull(Diagnostics.convert(error)); - } - - @Test - void convert_line1Char1_clampedToZero() { - // Line 1, char 1 → line 0, char 0 (max(..., 0) prevents negative) - TrackBack tb = locator(1, 1, 1, 1); - Diagnostic d = Diagnostics.convert(exception("msg", tb, CqlCompilerException.ErrorSeverity.Error)); - - assertNotNull(d); - assertEquals(0, d.getRange().getStart().getLine()); - assertEquals(0, d.getRange().getStart().getCharacter()); - assertEquals(0, d.getRange().getEnd().getLine()); - assertEquals(1, d.getRange().getEnd().getCharacter()); // endChar = 1 (no -1) - } - - @Test - void convert_messagePropagated() { - TrackBack tb = locator(2, 1, 2, 5); - Diagnostic d = Diagnostics.convert(exception("my error message", tb, CqlCompilerException.ErrorSeverity.Error)); - - assertEquals("my error message", d.getMessage()); - } - - // ----------------------------------------------------------------------- - // severity mapping - // ----------------------------------------------------------------------- - - @Test - void severity_error_mapsToError() { - TrackBack tb = locator(1, 1, 1, 5); - Diagnostic d = Diagnostics.convert(exception("msg", tb, CqlCompilerException.ErrorSeverity.Error)); - assertEquals(DiagnosticSeverity.Error, d.getSeverity()); - } - - @Test - void severity_warning_mapsToWarning() { - TrackBack tb = locator(1, 1, 1, 5); - Diagnostic d = Diagnostics.convert(exception("msg", tb, CqlCompilerException.ErrorSeverity.Warning)); - assertEquals(DiagnosticSeverity.Warning, d.getSeverity()); - } - - @Test - void severity_info_mapsToInformation() { - TrackBack tb = locator(1, 1, 1, 5); - Diagnostic d = Diagnostics.convert(exception("msg", tb, CqlCompilerException.ErrorSeverity.Info)); - assertEquals(DiagnosticSeverity.Information, d.getSeverity()); - } - - // ----------------------------------------------------------------------- - // convert(Iterable) - // ----------------------------------------------------------------------- - - @Test - void convertIterable_emptyList_returnsEmptySet() { - Set result = Diagnostics.convert(Collections.emptyList()); - assertNotNull(result); - assertTrue(result.isEmpty()); - } - - @Test - void convertIterable_mixedLocators_onlyLocatedIncluded() { - CqlSemanticException located = - exception("located", locator(1, 1, 1, 10), CqlCompilerException.ErrorSeverity.Error); - CqlSemanticException unlocated = - new CqlSemanticException("unlocated", null, CqlCompilerException.ErrorSeverity.Error, null); - - Set result = Diagnostics.convert(Arrays.asList(located, unlocated)); - - assertEquals(1, result.size()); - } - - @Test - void convertIterable_twoLocatedErrors_bothIncluded() { - CqlSemanticException e1 = exception("first", locator(1, 1, 1, 5), CqlCompilerException.ErrorSeverity.Error); - CqlSemanticException e2 = exception("second", locator(2, 1, 2, 5), CqlCompilerException.ErrorSeverity.Error); - - Set result = Diagnostics.convert(Arrays.asList(e1, e2)); - - assertEquals(2, result.size()); - } -} diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.java b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.java deleted file mode 100644 index 042b083e..00000000 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.opencds.cqf.cql.ls.server.visitor; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -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; -import org.hl7.elm.r1.Retrieve; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.cql.ls.core.ContentService; -import org.opencds.cqf.cql.ls.core.utility.Uris; -import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.manager.IgContextManager; -import org.opencds.cqf.cql.ls.server.service.TestContentService; - -class ExpressionTrackBackVisitorTest { - private static Library library; - - @BeforeAll - static void beforeAll() { - ContentService cs = new TestContentService(); - CqlCompilationManager cqlCompilationManager = - new CqlCompilationManager(cs, new CompilerOptionsManager(cs), new IgContextManager(cs)); - library = cqlCompilationManager - .compile(Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.cql")) - .getCompiledLibrary() - .getLibrary(); - } - - @Test - void positionInRetrieve_returnsRetrieve() { - ExpressionTrackBackVisitor visitor = new ExpressionTrackBackVisitor(); - TrackBack tb = new TrackBack(library.getIdentifier(), 9, 9, 9, 9); - Element e = visitor.visitLibrary(library, tb); - assertNotNull(e); - assertThat(e, instanceOf(Retrieve.class)); - } - - @Test - void positionOutsideExpression_returnsNull() { - ExpressionTrackBackVisitor visitor = new ExpressionTrackBackVisitor(); - TrackBack tb = new TrackBack(library.getIdentifier(), 10, 0, 10, 0); - Element e = visitor.visitLibrary(library, tb); - assertNull(e); - } - - @Test - void positionInExpression_returnsExpressionDef() { - ExpressionTrackBackVisitor visitor = new ExpressionTrackBackVisitor(); - TrackBack tb = new TrackBack(library.getIdentifier(), 15, 10, 15, 10); - Element e = visitor.visitLibrary(library, tb); - assertNotNull(e); - assertThat(e, instanceOf(ExpressionDef.class)); - } - - @Test - void positionInExpressionRef_returnsExpressionDef() { - // Line 12 (1-indexed): " "ObservationRetrieve"" — the ExpressionRef inside ObservationReference - ExpressionTrackBackVisitor visitor = new ExpressionTrackBackVisitor(); - TrackBack tb = new TrackBack(library.getIdentifier(), 12, 5, 12, 25); - Element e = visitor.visitLibrary(library, tb); - assertNotNull(e); - assertThat(e, instanceOf(ExpressionDef.class)); - } - - @Test - void positionAtExpressionBoundary_returnsExpressionDef() { - // Line 15 (1-indexed): " Patient.birthDate" — at the start char of PropertyAccess body - ExpressionTrackBackVisitor visitor = new ExpressionTrackBackVisitor(); - TrackBack tb = new TrackBack(library.getIdentifier(), 15, 5, 15, 5); - Element e = visitor.visitLibrary(library, tb); - assertNotNull(e); - assertThat(e, instanceOf(ExpressionDef.class)); - } -} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContributionTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContributionTest.kt new file mode 100644 index 00000000..69f6c244 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContributionTest.kt @@ -0,0 +1,134 @@ +package org.opencds.cqf.cql.ls.server.command + +import com.google.gson.JsonParser +import org.eclipse.lsp4j.ExecuteCommandParams +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import org.opencds.cqf.cql.ls.server.service.TestContentService +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Paths +import java.util.concurrent.CompletableFuture + +class ViewElmCommandContributionTest { + companion object { + private lateinit var viewElmCommandContribution: ViewElmCommandContribution + + @BeforeAll + @JvmStatic + fun beforeAll() { + val cs = TestContentService() + val cqlCompilationManager = + CqlCompilationManager(cs, CompilerOptionsManager(cs), IgContextManager(cs)) + viewElmCommandContribution = ViewElmCommandContribution(cqlCompilationManager) + } + } + + @Test + fun getCommands() { + assertEquals(1, viewElmCommandContribution.getCommands().size) + assertEquals( + "org.opencds.cqf.cql.ls.viewElm", + viewElmCommandContribution.getCommands().toTypedArray()[0], + ) + } + + @Test + fun executeCommand() { + val params = ExecuteCommandParams() + params.command = "org.opencds.cqf.cql.ls.viewElm" + params.arguments = + listOf( + JsonParser.parseString("\"\\/org\\/opencds\\/cqf\\/cql\\/ls\\/server\\/One.cql\""), + ) + val future: CompletableFuture = + viewElmCommandContribution + .executeCommand(params) + .thenAccept { result -> + try { + val expectedXml = + String( + Files.readAllBytes( + Paths.get("src/test/resources/org/opencds/cqf/cql/ls/server/One.xml"), + ), + ) + .trim() + .replace("\\s+".toRegex(), "") + assertEquals(expectedXml, result.toString().trim().replace("\\s+".toRegex(), "")) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + // This ensures the test waits and fails if an exception occurs + future.join() + } + + @Test + fun executeCommandWithXmlElmType() { + val params = ExecuteCommandParams() + params.command = "org.opencds.cqf.cql.ls.viewElm" + params.arguments = + listOf( + JsonParser.parseString("\"\\/org\\/opencds\\/cqf\\/cql\\/ls\\/server\\/One.cql\""), + JsonParser.parseString("\"xml\""), + ) + val future: CompletableFuture = + viewElmCommandContribution + .executeCommand(params) + .thenAccept { result -> + try { + val expectedXml = + String( + Files.readAllBytes( + Paths.get("src/test/resources/org/opencds/cqf/cql/ls/server/One.xml"), + ), + ) + .trim() + .replace("\\s+".toRegex(), "") + assertEquals(expectedXml, result.toString().trim().replace("\\s+".toRegex(), "")) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + // This ensures the test waits and fails if an exception occurs + future.join() + } + + @Test + fun executeCommandWithJsonElmType() { + val params = ExecuteCommandParams() + params.command = "org.opencds.cqf.cql.ls.viewElm" + params.arguments = + listOf( + JsonParser.parseString("\"\\/org\\/opencds\\/cqf\\/cql\\/ls\\/server\\/One.cql\""), + JsonParser.parseString("\"json\""), + ) + val future: CompletableFuture = + viewElmCommandContribution + .executeCommand(params) + .thenAccept { result -> + try { + val expectedJson = + String( + Files.readAllBytes( + Paths.get("src/test/resources/org/opencds/cqf/cql/ls/server/One.json"), + ), + ) + .trim() + .replace("\\s+".toRegex(), "") + assertEquals(expectedJson, result.toString().trim().replace("\\s+".toRegex(), "")) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + // This ensures the test waits and fails if an exception occurs + future.join() + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt new file mode 100644 index 00000000..664926d3 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt @@ -0,0 +1,122 @@ +package org.opencds.cqf.cql.ls.server.manager + +import org.cqframework.cql.cql2elm.CqlCompilerOptions +import org.cqframework.cql.cql2elm.LibraryBuilder.SignatureLevel +import org.eclipse.lsp4j.DidChangeWatchedFilesParams +import org.eclipse.lsp4j.FileChangeType +import org.eclipse.lsp4j.FileEvent +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent +import org.opencds.cqf.cql.ls.server.service.TestContentService +import java.net.URI + +class CompilerOptionsManagerTest { + private lateinit var manager: CompilerOptionsManager + + @BeforeEach + fun setUp() { + val cs: ContentService = TestContentService() + manager = CompilerOptionsManager(cs) + } + + // ----------------------------------------------------------------------- + // getOptions — returns options enriched with required flags + // ----------------------------------------------------------------------- + + @Test + fun getOptions_noOptionsFile_returnsNonNull() { + val options = manager.getOptions(TEST_URI) + assertNotNull(options) + } + + @Test + fun getOptions_alwaysIncludesEnableLocators() { + val options = manager.getOptions(TEST_URI) + assertTrue(options.options.contains(CqlCompilerOptions.Options.EnableLocators)) + } + + @Test + fun getOptions_alwaysIncludesEnableResultTypes() { + val options = manager.getOptions(TEST_URI) + assertTrue(options.options.contains(CqlCompilerOptions.Options.EnableResultTypes)) + } + + @Test + fun getOptions_alwaysIncludesEnableAnnotations() { + val options = manager.getOptions(TEST_URI) + assertTrue(options.options.contains(CqlCompilerOptions.Options.EnableAnnotations)) + } + + @Test + fun getOptions_signatureLevelIsAll() { + val options = manager.getOptions(TEST_URI) + assertEquals(SignatureLevel.All, options.signatureLevel) + } + + // ----------------------------------------------------------------------- + // caching — same instance returned on second call + // ----------------------------------------------------------------------- + + @Test + fun getOptions_secondCall_returnsCachedInstance() { + val first = manager.getOptions(TEST_URI) + val second = manager.getOptions(TEST_URI) + assertSame(first, second) + } + + // ----------------------------------------------------------------------- + // clearOptions — evicts cache so next call re-reads + // ----------------------------------------------------------------------- + + @Test + fun clearOptions_evictsCache() { + manager.getOptions(TEST_URI) // populate cache + manager.clearOptions(TEST_URI) + val second = manager.getOptions(TEST_URI) + assertTrue(second.options.contains(CqlCompilerOptions.Options.EnableLocators)) + } + + // ----------------------------------------------------------------------- + // onMessageEvent — cql-options.json change clears cache + // ----------------------------------------------------------------------- + + @Test + fun onMessageEvent_cqlOptionsChanged_clearsCache() { + manager.getOptions(TEST_URI) // populate cache + + val optionsUri = Uris.getHead(TEST_URI).toString() + "/cql/cql-options.json" + val fileEvent = FileEvent(optionsUri, FileChangeType.Changed) + val params = DidChangeWatchedFilesParams(listOf(fileEvent)) + manager.onMessageEvent(DidChangeWatchedFilesEvent(params)) + + val second = manager.getOptions(TEST_URI) + assertNotNull(second) + assertTrue(second.options.contains(CqlCompilerOptions.Options.EnableLocators)) + } + + @Test + fun onMessageEvent_unrelatedFile_doesNotClearCache() { + val first = manager.getOptions(TEST_URI) + + val fileEvent = FileEvent("file:///workspace/SomeOtherFile.json", FileChangeType.Changed) + val params = DidChangeWatchedFilesParams(listOf(fileEvent)) + manager.onMessageEvent(DidChangeWatchedFilesEvent(params)) + + val second = manager.getOptions(TEST_URI) + assertSame(first, second) + } + + companion object { + // A URI whose "head" (parent path) is /org/opencds/cqf/cql/ls/server/ + // TestContentService.read will be called for the cql-options.json path, returning null + // (no such classpath resource), so default options are used. + private val TEST_URI: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/One.cql")!! + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.kt new file mode 100644 index 00000000..dba3b922 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceProviderTest.kt @@ -0,0 +1,33 @@ +package org.opencds.cqf.cql.ls.server.provider + +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertThrows +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 + +class ContentServiceProviderTest { + @Test + fun should_throwException_when_gettingLibrary() { + val versionedIdentifier = VersionedIdentifier() + versionedIdentifier.withVersion("1.0.0") + + val contentServiceSourceProvider = + ContentServiceSourceProvider( + Uris.parseOrNull("/provider/content/sample-library-1.0.0.json")!!, + object : ContentService { + override fun locate( + root: URI, + identifier: VersionedIdentifier, + ): Set { + throw UncheckedIOException(IOException()) + } + }, + ) + + assertThrows(RuntimeException::class.java) { contentServiceSourceProvider.getLibrarySource(versionedIdentifier) } + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProviderTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProviderTest.kt new file mode 100644 index 00000000..d5aa5e8c --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceSourceProviderTest.kt @@ -0,0 +1,45 @@ +package org.opencds.cqf.cql.ls.server.provider + +import kotlinx.io.Source +import org.cqframework.cql.cql2elm.LibrarySourceProvider +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.server.service.TestContentService +import java.net.URI + +class ContentServiceSourceProviderTest { + @Test + fun getLibrarySource_knownLibrary_returnsSource() { + val id = VersionedIdentifier().withId("One") + val source: Source? = provider.getLibrarySource(id) + assertNotNull(source) + } + + @Test + fun getLibrarySource_unknownLibrary_returnsNull() { + val id = VersionedIdentifier().withId("DoesNotExistLibrary") + val source: Source? = provider.getLibrarySource(id) + assertNull(source) + } + + @Test + fun getLibrarySource_implementsLibrarySourceProvider() { + assertNotNull(provider as LibrarySourceProvider) + } + + companion object { + private lateinit var provider: ContentServiceSourceProvider + + @BeforeAll + @JvmStatic + fun beforeAll() { + val cs: ContentService = TestContentService() + val root = URI.create("file:///workspace/") + provider = ContentServiceSourceProvider(root, cs) + } + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/FormattingProviderTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/FormattingProviderTest.kt new file mode 100644 index 00000000..a1bd7298 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/FormattingProviderTest.kt @@ -0,0 +1,41 @@ +package org.opencds.cqf.cql.ls.server.provider + +import org.eclipse.lsp4j.Position +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.server.service.TestContentService + +class FormattingProviderTest { + @Test + fun format_validCql_returnsOneEdit() { + val edits = formattingProvider.format("/org/opencds/cqf/cql/ls/server/Two.cql") + assertEquals(1, edits.size) + val edit = edits[0] + assertEquals(Position(0, 0), edit.range.start) + assertNotNull(edit.newText) + assertFalse(edit.newText.isBlank()) + } + + @Test + fun format_syntaxError_throwsIllegalArgument() { + assertThrows(IllegalArgumentException::class.java) { + formattingProvider.format("/org/opencds/cqf/cql/ls/server/SyntaxError.cql") + } + } + + companion object { + private lateinit var formattingProvider: FormattingProvider + + @BeforeAll + @JvmStatic + fun beforeAll() { + val cs: ContentService = TestContentService() + formattingProvider = FormattingProvider(cs) + } + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/HoverProviderTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/HoverProviderTest.kt new file mode 100644 index 00000000..1e120631 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/HoverProviderTest.kt @@ -0,0 +1,117 @@ +package org.opencds.cqf.cql.ls.server.provider + +import org.eclipse.lsp4j.HoverParams +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import org.opencds.cqf.cql.ls.server.service.TestContentService + +class HoverProviderTest { + companion object { + private lateinit var hoverProvider: HoverProvider + + @BeforeAll + @JvmStatic + fun beforeAll() { + val cs = TestContentService() + val cqlCompilationManager = + CqlCompilationManager(cs, CompilerOptionsManager(cs), IgContextManager(cs)) + hoverProvider = HoverProvider(cqlCompilationManager) + } + } + + @Test + fun hoverInt() { + val hover = + hoverProvider.hover( + HoverParams( + TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), + Position(5, 2), + ), + ) + + assertNotNull(hover) + assertNotNull(hover!!.contents.right) + + val markup = hover.contents.right + assertEquals("markdown", markup.kind) + assertEquals("```cql\nSystem.Integer\n```", markup.value) + } + + @Test + fun hoverNothing() { + val hover = + hoverProvider.hover( + HoverParams( + TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), + Position(2, 0), + ), + ) + + assertNull(hover) + } + + @Test + fun hoverList() { + val hover = + hoverProvider.hover( + HoverParams( + TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), + Position(8, 2), + ), + ) + + assertNotNull(hover) + assertNotNull(hover!!.contents.right) + + val markup = hover.contents.right + assertEquals("markdown", markup.kind) + assertEquals("```cql\nlist\n```", markup.value) + } + + @Test + fun hoverOnLibraryRef() { + // Line 5 (0-indexed): " 1 + One."One"" — position (5, 8) is 'O' in 'One' + val hover = + hoverProvider.hover( + HoverParams( + TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), + Position(5, 8), + ), + ) + + assertNotNull(hover) + assertNotNull(hover!!.contents.right) + + val markup = hover.contents.right + assertEquals("markdown", markup.kind) + assertEquals("```cql\nSystem.Integer\n```", markup.value) + } + + @Test + fun hoverOnDefineName() { + // Line 4 (0-indexed): "define "Two":" — position (4, 8) is 'T' inside the define name + val hover = + hoverProvider.hover( + HoverParams( + TextDocumentIdentifier("/org/opencds/cqf/cql/ls/server/Two.cql"), + Position(4, 8), + ), + ) + + // ExpressionDef TrackBack covers the define header — expect Integer type + assertNotNull(hover) + assertNotNull(hover!!.contents.right) + + val markup = hover.contents.right + assertEquals("markdown", markup.kind) + assertEquals("```cql\nSystem.Integer\n```", markup.value) + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/BadDataTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/BadDataTest.kt new file mode 100644 index 00000000..8218b022 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/BadDataTest.kt @@ -0,0 +1,76 @@ +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.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.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +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 +import java.nio.file.Path +import java.util.stream.Stream + +class BadDataTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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 = IgStandardRepository(FhirContext.forR4Cached(), tempDir!!) + } + + @JvmStatic + fun invalidContentTestData(): Stream { + return Stream.of( + Arguments.of(IdType("Patient/InvalidContent"), "Found empty or invalid content"), + Arguments.of(IdType("Patient/MissingId"), "Found resource without an id"), + Arguments.of(IdType("Patient/NoContent"), "Found empty or invalid content"), + Arguments.of(IdType("Patient/WrongId"), "Found resource with an id DoesntMatchFilename"), + Arguments.of(IdType("Patient/WrongResourceType"), "Found resource with type Encounter"), + Arguments.of(IdType("Patient/WrongVersion").withVersion("1"), "Found resource with version 2"), + ) + } + } + + @ParameterizedTest + @MethodSource("invalidContentTestData") + fun readInvalidContentThrowsException( + id: IIdType, + errorMessage: String, + ) { + val e = assertThrows(ResourceNotFoundException::class.java) { repository.read(Patient::class.java, id) } + assertTrue(e.message!!.contains(errorMessage)) + } + + @Test + fun nonFhirFilesAreIgnored() { + val id = IdType("Patient/NotAFhirFile") + assertThrows(ResourceNotFoundException::class.java) { repository.read(Patient::class.java, id) } + } + + @Test + fun searchThrowsBecauseOfInvalidContent() { + // If there's any invalid content in the directory, the search will fail + assertThrows(ResourceNotFoundException::class.java) { + repository.search(Bundle::class.java, Patient::class.java, Searches.ALL) + } + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/CompartmentTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/CompartmentTest.kt new file mode 100644 index 00000000..34995040 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/CompartmentTest.kt @@ -0,0 +1,260 @@ +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.instance.model.api.IIdType +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.Encounter +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.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +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 +import java.nio.file.Files +import java.nio.file.Path + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class CompartmentTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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 = IgStandardRepository(FhirContext.forR4Cached(), tempDir!!) + } + } + + @Test + fun readLibrary() { + val id: IIdType = Ids.newId(Library::class.java, "123") + val lib = repository.read(Library::class.java, id) + assertNotNull(lib) + assertEquals(id.idPart, lib.idElement.idPart) + } + + @Test + fun readLibraryNotExists() { + val id: IIdType = Ids.newId(Library::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.read(Library::class.java, id) } + } + + @Test + fun searchLibrary() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.ALL) + + assertNotNull(libs) + assertEquals(2, libs.entry.size) + } + + @Test + fun searchLibraryNotExists() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("not-exists")) + assertNotNull(libs) + assertEquals(0, libs.entry.size) + } + + @Test + fun readPatientNoCompartment() { + val id: IIdType = Ids.newId(Patient::class.java, "123") + assertThrows(ResourceNotFoundException::class.java) { repository.read(Patient::class.java, id) } + } + + @Test + fun readPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "123") + val p = repository.read(Patient::class.java, id, mapOf(IgStandardRepository.FHIR_COMPARTMENT_HEADER to "Patient/123")) + + assertNotNull(p) + assertEquals(id.idPart, p.idElement.idPart) + } + + @Test + fun searchEncounterNoCompartment() { + val encounters = repository.search(Bundle::class.java, Encounter::class.java, Searches.ALL) + assertNotNull(encounters) + assertEquals(0, encounters.entry.size) + } + + @Test + fun searchEncounter() { + val encounters = + repository.search( + Bundle::class.java, + Encounter::class.java, + Searches.ALL, + mapOf(IgStandardRepository.FHIR_COMPARTMENT_HEADER to "Patient/123"), + ) + assertNotNull(encounters) + assertEquals(1, encounters.entry.size) + } + + @Test + fun readValueSetNoCompartment() { + val id: IIdType = Ids.newId(ValueSet::class.java, "456") + val vs = repository.read(ValueSet::class.java, id) + + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + } + + // Terminology resources are not in compartments + @Test + fun readValueSet() { + val id: IIdType = Ids.newId(ValueSet::class.java, "456") + val vs = + repository.read( + ValueSet::class.java, + id, + mapOf(IgStandardRepository.FHIR_COMPARTMENT_HEADER to "Patient/123"), + ) + + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + } + + @Test + fun searchValueSet() { + val sets = repository.search(Bundle::class.java, ValueSet::class.java, Searches.byUrl("example.com/ValueSet/456")) + assertNotNull(sets) + assertEquals(1, sets.entry.size) + } + + @Test + fun createAndDeleteLibrary() { + val lib = Library() + lib.id = "new-library" + val o = repository.create(lib) + val created = repository.read(Library::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("resources/library/new-library.json") + assertTrue(Files.exists(loc)) + + repository.delete(Library::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeletePatient() { + val p = Patient() + p.id = "new-patient" + val header = mapOf(IgStandardRepository.FHIR_COMPARTMENT_HEADER to "Patient/new-patient") + val o = repository.create(p, header) + val created = repository.read(Patient::class.java, o.id!!, header) + assertNotNull(created) + + val loc = tempDir!!.resolve("tests/patient/new-patient/patient/new-patient.json") + assertTrue(Files.exists(loc)) + + repository.delete(Patient::class.java, created.idElement, header) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeleteValueSet() { + val v = ValueSet() + v.id = "new-valueset" + val o = repository.create(v) + val created = repository.read(ValueSet::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("vocabulary/valueset/new-valueset.json") + assertTrue(Files.exists(loc)) + + repository.delete(ValueSet::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun updatePatient() { + val id: IIdType = Ids.newId(Patient::class.java, "123") + val p = repository.read(Patient::class.java, id, mapOf(IgStandardRepository.FHIR_COMPARTMENT_HEADER to "Patient/123")) + assertFalse(p.hasActive()) + + p.active = true + repository.update(p) + + val updated = repository.read(Patient::class.java, id, mapOf(IgStandardRepository.FHIR_COMPARTMENT_HEADER to "Patient/123")) + assertTrue(updated.hasActive()) + assertTrue(updated.active) + } + + @Test + fun deleteNonExistentPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.delete(Patient::class.java, id) } + } + + @Test + fun searchNonExistentType() { + val results = repository.search(Bundle::class.java, Encounter::class.java, Searches.ALL) + assertNotNull(results) + assertEquals(0, results.entry.size) + } + + @Test + fun searchById() { + val bundle = repository.search(Bundle::class.java, Library::class.java, Searches.byId("123")) + assertNotNull(bundle) + assertEquals(1, bundle.entry.size) + } + + @Test + fun searchByIdNotFound() { + val bundle = repository.search(Bundle::class.java, Library::class.java, Searches.byId("DoesNotExist")) + assertNotNull(bundle) + assertEquals(0, bundle.entry.size) + } + + @Test + @Order(1) // Do this test first because it puts the filesystem (temporarily) in an invalid state + fun resourceMissingWhenCacheCleared() { + val id = org.hl7.fhir.r4.model.IdType("Library", "ToDelete") + var lib = Library().setIdElement(id) + val path = tempDir!!.resolve("resources/library/ToDelete.json") + + repository.create(lib) + assertTrue(path.toFile().exists()) + + // Read back, should exist + lib = repository.read(Library::class.java, 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.java, id) + assertNotNull(lib) + assertEquals("ToDelete", lib.idElement.idPart) + + (repository as IgStandardRepository).clearCache() + + // Try to read again, should be gone because it's not in the cache and the content is gone. + assertThrows(ResourceNotFoundException::class.java) { repository.read(Library::class.java, id) } + + // Clean up so that we don't affect other tests + path.toFile().delete() + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.kt new file mode 100644 index 00000000..41546901 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/ConventionsTest.kt @@ -0,0 +1,94 @@ +package org.opencds.cqf.cql.ls.server.repository.ig.standard + +import org.junit.jupiter.api.Assertions.assertEquals +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.nio.file.Path + +class ConventionsTest { + companion object { + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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 + fun autoDetectDefault() { + assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(null)) + assertEquals( + IgStandardConventions.STANDARD, + IgStandardConventions.autoDetect(tempDir!!.resolve("does_not_exist")), + ) + } + + @Test + fun autoDetectStandard() { + assertEquals( + IgStandardConventions.STANDARD, + IgStandardConventions.autoDetect(tempDir!!.resolve("directoryPerType/standard")), + ) + } + + @Test + fun autoDetectPrefix() { + val 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 + fun autoDetectFlat() { + assertEquals(IgStandardConventions.FLAT, IgStandardConventions.autoDetect(tempDir!!.resolve("flat"))) + } + + @Test + fun autoDetectFlatNoTypeNames() { + val 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 + fun autoDetectWithMisleadingFileName() { + assertEquals( + IgStandardConventions.STANDARD, + IgStandardConventions.autoDetect(tempDir!!.resolve("misleadingFileName")), + ) + } + + @Test + fun autoDetectWithEmptyContent() { + assertEquals(IgStandardConventions.STANDARD, IgStandardConventions.autoDetect(tempDir!!.resolve("emptyContent"))) + } + + @Test + fun autoDetectWithNonFhirFilename() { + assertEquals( + IgStandardConventions.STANDARD, + IgStandardConventions.autoDetect(tempDir!!.resolve("nonFhirFilename")), + ) + } + + @Test + fun autoDetectWitCompartments() { + val 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/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.kt new file mode 100644 index 00000000..11373087 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/CqlContentTest.kt @@ -0,0 +1,96 @@ +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.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +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.nio.file.Path + +class CqlContentTest { + companion object { + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar("/sampleIgs/ig/standard/cqlContent", tempDir!!) + } + } + + @Test + fun loadCqlContentDstu3() { + val lib = Library() + lib.addContent().setContentType("text/cql").url = "cql/Test.cql" + IgStandardCqlContent.loadCqlContent(lib, tempDir!!) + assertNotNull(lib.contentFirstRep.data) + } + + @Test + fun loadCqlContentR4() { + val lib = org.hl7.fhir.r4.model.Library() + lib.addContent().setContentType("text/cql").url = "cql/Test.cql" + IgStandardCqlContent.loadCqlContent(lib, tempDir!!) + assertNotNull(lib.contentFirstRep.data) + } + + @Test + fun loadCqlContentR5() { + val lib = org.hl7.fhir.r5.model.Library() + lib.addContent().setContentType("text/cql").url = "cql/Test.cql" + IgStandardCqlContent.loadCqlContent(lib, tempDir!!) + assertNotNull(lib.contentFirstRep.data) + } + + @Test + fun emptyLibraryDoesNothing() { + val lib = Library() + IgStandardCqlContent.loadCqlContent(lib, tempDir!!) + assertEquals(0, lib.content.size) + } + + @Test + fun nonLibraryResourceDoesNotThrow() { + assertDoesNotThrow { IgStandardCqlContent.loadCqlContent(ValueSet(), tempDir!!) } + } + + @Test + fun invalidFhirVersionThrows() { + val lib = org.hl7.fhir.r4b.model.Library() + assertThrows(IllegalArgumentException::class.java) { IgStandardCqlContent.loadCqlContent(lib, tempDir!!) } + } + + @Test + fun invalidPathThrows() { + val lib = org.hl7.fhir.r4.model.Library() + lib.addContent().setContentType("text/cql").url = "not-a-real-path/Test.cql" + assertThrows(ResourceNotFoundException::class.java) { IgStandardCqlContent.loadCqlContent(lib, tempDir!!) } + } + + @Suppress("ARGUMENT_TYPE_MISMATCH") + @Test + fun nullThrows() { + @Suppress("ARGUMENT_TYPE_MISMATCH") + assertThrows(NullPointerException::class.java) { + @Suppress("ARGUMENT_TYPE_MISMATCH") + IgStandardCqlContent.loadCqlContent(null as org.hl7.fhir.instance.model.api.IBaseResource?, tempDir!!) + } + + val lib = Library() + @Suppress("ARGUMENT_TYPE_MISMATCH") + assertThrows(NullPointerException::class.java) { + @Suppress("ARGUMENT_TYPE_MISMATCH") + IgStandardCqlContent.loadCqlContent(lib, null as Path?) + } + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.kt new file mode 100644 index 00000000..69ff8875 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/DirectoryTest.kt @@ -0,0 +1,190 @@ +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.instance.model.api.IIdType +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.Condition +import org.hl7.fhir.r4.model.Encounter +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.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +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 +import java.nio.file.Files +import java.nio.file.Path + +class DirectoryTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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 = IgStandardRepository(FhirContext.forR4Cached(), tempDir!!) + } + } + + @Test + fun readLibrary() { + val id: IIdType = Ids.newId(Library::class.java, "123") + val lib = repository.read(Library::class.java, id) + assertNotNull(lib) + assertEquals(id.idPart, lib.idElement.idPart) + } + + @Test + fun readLibraryNotExists() { + val id: IIdType = Ids.newId(Library::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.read(Library::class.java, id) } + } + + @Test + fun searchLibrary() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.ALL) + + assertNotNull(libs) + assertEquals(2, libs.entry.size) + } + + @Test + fun searchLibraryWithFilter() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("http://example.com/Library/Test")) + + assertNotNull(libs) + assertEquals(1, libs.entry.size) + } + + @Test + fun searchLibraryNotExists() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("not-exists")) + assertNotNull(libs) + assertEquals(0, libs.entry.size) + } + + @Test + fun readPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "ABC") + val cond = repository.read(Patient::class.java, id) + + assertNotNull(cond) + assertEquals(id.idPart, cond.idElement.idPart) + } + + @Test + fun searchCondition() { + val cons = + repository.search( + Bundle::class.java, + Condition::class.java, + Searches.byCodeAndSystem("12345", "example.com/codesystem"), + ) + assertNotNull(cons) + assertEquals(2, cons.entry.size) + } + + @Test + fun readValueSet() { + val id: IIdType = Ids.newId(ValueSet::class.java, "456") + val vs = repository.read(ValueSet::class.java, id) + + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + } + + @Test + fun searchValueSet() { + val sets = repository.search(Bundle::class.java, ValueSet::class.java, Searches.byUrl("example.com/ValueSet/456")) + assertNotNull(sets) + assertEquals(1, sets.entry.size) + } + + @Test + fun createAndDeleteLibrary() { + val lib = Library() + lib.id = "new-library" + val o = repository.create(lib) + val created = repository.read(Library::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("resources/library/new-library.json") + assertTrue(Files.exists(loc)) + + repository.delete(Library::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeletePatient() { + val p = Patient() + p.id = "new-patient" + val o = repository.create(p) + val created = repository.read(Patient::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("tests/patient/new-patient.json") + assertTrue(Files.exists(loc)) + + repository.delete(Patient::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeleteValueSet() { + val v = ValueSet() + v.id = "new-valueset" + val o = repository.create(v) + val created = repository.read(ValueSet::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("vocabulary/valueset/new-valueset.json") + assertTrue(Files.exists(loc)) + + repository.delete(ValueSet::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun updatePatient() { + val id: IIdType = Ids.newId(Patient::class.java, "ABC") + val p = repository.read(Patient::class.java, id) + assertFalse(p.hasActive()) + + p.active = true + repository.update(p) + + val updated = repository.read(Patient::class.java, id) + assertTrue(updated.hasActive()) + assertTrue(updated.active) + } + + @Test + fun deleteNonExistentPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.delete(Patient::class.java, id) } + } + + @Test + fun searchNonExistentType() { + val results = repository.search(Bundle::class.java, Encounter::class.java, Searches.ALL) + assertNotNull(results) + assertEquals(0, results.entry.size) + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/ExternalTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/ExternalTest.kt new file mode 100644 index 00000000..75b05f76 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/ExternalTest.kt @@ -0,0 +1,92 @@ +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.ForbiddenOperationException +import org.hl7.fhir.instance.model.api.IIdType +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.ValueSet +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +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 +import java.nio.file.Files +import java.nio.file.Path + +class ExternalTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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 = IgStandardRepository(FhirContext.forR4Cached(), tempDir!!) + } + } + + @Test + fun readValueSet() { + val id: IIdType = Ids.newId(ValueSet::class.java, "456") + val vs = repository.read(ValueSet::class.java, id) + + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + } + + @Test + fun createAndDeleteValueSet() { + val v = ValueSet() + v.id = "new-valueset" + val o = repository.create(v) + val created = repository.read(ValueSet::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("vocabulary/valueset/new-valueset.json") + assertTrue(Files.exists(loc)) + + repository.delete(ValueSet::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun readExternalValueSet() { + val id: IIdType = Ids.newId(ValueSet::class.java, "789") + val vs = repository.read(ValueSet::class.java, id) + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + + // Should be tagged with its source path + val path = vs.getUserData(IgStandardRepository.SOURCE_PATH_TAG) as Path + assertNotNull(path) + assertTrue(path.toFile().exists()) + assertTrue(path.toString().contains("external")) + } + + @Test + fun searchExternalValueSet() { + val sets = repository.search(Bundle::class.java, ValueSet::class.java, Searches.byUrl("example.com/ValueSet/789")) + assertNotNull(sets) + assertEquals(1, sets.entry.size) + } + + @Test + fun updateExternalValueSetFails() { + val id: IIdType = Ids.newId(ValueSet::class.java, "789") + val vs = repository.read(ValueSet::class.java, id) + assertThrows(ForbiddenOperationException::class.java) { repository.update(vs) } + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatNoTypeNamesTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatNoTypeNamesTest.kt new file mode 100644 index 00000000..3e75c866 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatNoTypeNamesTest.kt @@ -0,0 +1,190 @@ +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.instance.model.api.IIdType +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.Condition +import org.hl7.fhir.r4.model.Encounter +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.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +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 +import java.nio.file.Files +import java.nio.file.Path + +class FlatNoTypeNamesTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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 = IgStandardRepository(FhirContext.forR4Cached(), tempDir!!) + } + } + + @Test + fun readLibrary() { + val id: IIdType = Ids.newId(Library::class.java, "123") + val lib = repository.read(Library::class.java, id) + assertNotNull(lib) + assertEquals(id.idPart, lib.idElement.idPart) + } + + @Test + fun readLibraryNotExists() { + val id: IIdType = Ids.newId(Library::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.read(Library::class.java, id) } + } + + @Test + fun searchLibrary() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.ALL) + + assertNotNull(libs) + assertEquals(2, libs.entry.size) + } + + @Test + fun searchLibraryWithFilter() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("http://example.com/Library/Test")) + + assertNotNull(libs) + assertEquals(1, libs.entry.size) + } + + @Test + fun searchLibraryNotExists() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("not-exists")) + assertNotNull(libs) + assertEquals(0, libs.entry.size) + } + + @Test + fun readPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "ABC") + val cond = repository.read(Patient::class.java, id) + + assertNotNull(cond) + assertEquals(id.idPart, cond.idElement.idPart) + } + + @Test + fun searchCondition() { + val cons = + repository.search( + Bundle::class.java, + Condition::class.java, + Searches.byCodeAndSystem("12345", "example.com/codesystem"), + ) + assertNotNull(cons) + assertEquals(2, cons.entry.size) + } + + @Test + fun readValueSet() { + val id: IIdType = Ids.newId(ValueSet::class.java, "789") + val vs = repository.read(ValueSet::class.java, id) + + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + } + + @Test + fun searchValueSet() { + val sets = repository.search(Bundle::class.java, ValueSet::class.java, Searches.byUrl("example.com/ValueSet/789")) + assertNotNull(sets) + assertEquals(1, sets.entry.size) + } + + @Test + fun createAndDeleteLibrary() { + val lib = Library() + lib.id = "new-library" + val o = repository.create(lib) + val created = repository.read(Library::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("new-library.json") + assertTrue(Files.exists(loc)) + + repository.delete(Library::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeletePatient() { + val p = Patient() + p.id = "new-patient" + val o = repository.create(p) + val created = repository.read(Patient::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("new-patient.json") + assertTrue(Files.exists(loc)) + + repository.delete(Patient::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeleteValueSet() { + val v = ValueSet() + v.id = "new-valueset" + val o = repository.create(v) + val created = repository.read(ValueSet::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("new-valueset.json") + assertTrue(Files.exists(loc)) + + repository.delete(ValueSet::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun updatePatient() { + val id: IIdType = Ids.newId(Patient::class.java, "ABC") + val p = repository.read(Patient::class.java, id) + assertFalse(p.hasActive()) + + p.active = true + repository.update(p) + + val updated = repository.read(Patient::class.java, id) + assertTrue(updated.hasActive()) + assertTrue(updated.active) + } + + @Test + fun deleteNonExistentPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.delete(Patient::class.java, id) } + } + + @Test + fun searchNonExistentType() { + val results = repository.search(Bundle::class.java, Encounter::class.java, Searches.ALL) + assertNotNull(results) + assertEquals(0, results.entry.size) + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatTest.kt new file mode 100644 index 00000000..dcbcc17d --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/FlatTest.kt @@ -0,0 +1,190 @@ +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.instance.model.api.IIdType +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.Condition +import org.hl7.fhir.r4.model.Encounter +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.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +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 +import java.nio.file.Files +import java.nio.file.Path + +class FlatTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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 = IgStandardRepository(FhirContext.forR4Cached(), tempDir!!) + } + } + + @Test + fun readLibrary() { + val id: IIdType = Ids.newId(Library::class.java, "123") + val lib = repository.read(Library::class.java, id) + assertNotNull(lib) + assertEquals(id.idPart, lib.idElement.idPart) + } + + @Test + fun readLibraryNotExists() { + val id: IIdType = Ids.newId(Library::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.read(Library::class.java, id) } + } + + @Test + fun searchLibrary() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.ALL) + + assertNotNull(libs) + assertEquals(2, libs.entry.size) + } + + @Test + fun searchLibraryWithFilter() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("http://example.com/Library/Test")) + + assertNotNull(libs) + assertEquals(1, libs.entry.size) + } + + @Test + fun searchLibraryNotExists() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("not-exists")) + assertNotNull(libs) + assertEquals(0, libs.entry.size) + } + + @Test + fun readPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "ABC") + val cond = repository.read(Patient::class.java, id) + + assertNotNull(cond) + assertEquals(id.idPart, cond.idElement.idPart) + } + + @Test + fun searchCondition() { + val cons = + repository.search( + Bundle::class.java, + Condition::class.java, + Searches.byCodeAndSystem("12345", "example.com/codesystem"), + ) + assertNotNull(cons) + assertEquals(2, cons.entry.size) + } + + @Test + fun readValueSet() { + val id: IIdType = Ids.newId(ValueSet::class.java, "456") + val vs = repository.read(ValueSet::class.java, id) + + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + } + + @Test + fun searchValueSet() { + val sets = repository.search(Bundle::class.java, ValueSet::class.java, Searches.byUrl("example.com/ValueSet/456")) + assertNotNull(sets) + assertEquals(1, sets.entry.size) + } + + @Test + fun createAndDeleteLibrary() { + val lib = Library() + lib.id = "new-library" + val o = repository.create(lib) + val created = repository.read(Library::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("Library-new-library.json") + assertTrue(Files.exists(loc)) + + repository.delete(Library::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeletePatient() { + val p = Patient() + p.id = "new-patient" + val o = repository.create(p) + val created = repository.read(Patient::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("Patient-new-patient.json") + assertTrue(Files.exists(loc)) + + repository.delete(Patient::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeleteValueSet() { + val v = ValueSet() + v.id = "new-valueset" + val o = repository.create(v) + val created = repository.read(ValueSet::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("ValueSet-new-valueset.json") + assertTrue(Files.exists(loc)) + + repository.delete(ValueSet::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun updatePatient() { + val id: IIdType = Ids.newId(Patient::class.java, "ABC") + val p = repository.read(Patient::class.java, id) + assertFalse(p.hasActive()) + + p.active = true + repository.update(p) + + val updated = repository.read(Patient::class.java, id) + assertTrue(updated.hasActive()) + assertTrue(updated.active) + } + + @Test + fun deleteNonExistentPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.delete(Patient::class.java, id) } + } + + @Test + fun searchNonExistentType() { + val results = repository.search(Bundle::class.java, Encounter::class.java, Searches.ALL) + assertNotNull(results) + assertEquals(0, results.entry.size) + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/MixedEncodingTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/MixedEncodingTest.kt new file mode 100644 index 00000000..d9ae06a1 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/MixedEncodingTest.kt @@ -0,0 +1,190 @@ +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.ForbiddenOperationException +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException +import org.hl7.fhir.instance.model.api.IIdType +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.Encounter +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.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +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 +import java.nio.file.Files +import java.nio.file.Path + +class MixedEncodingTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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 = IgStandardRepository(FhirContext.forR4Cached(), tempDir!!) + } + } + + @Test + fun readLibrary() { + val id: IIdType = Ids.newId(Library::class.java, "123") + val lib = repository.read(Library::class.java, id) + assertNotNull(lib) + assertEquals(id.idPart, lib.idElement.idPart) + } + + @Test + fun readLibraryNotExists() { + val id: IIdType = Ids.newId(Library::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.read(Library::class.java, id) } + } + + @Test + fun searchLibrary() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.ALL) + + assertNotNull(libs) + assertEquals(1, libs.entry.size) + } + + @Test + fun searchLibraryWithFilter() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("http://example.com/Library/123")) + + assertNotNull(libs) + assertEquals(1, libs.entry.size) + } + + @Test + fun searchLibraryNotExists() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("not-exists")) + assertNotNull(libs) + assertEquals(0, libs.entry.size) + } + + @Test + fun readValueSet() { + val id: IIdType = Ids.newId(ValueSet::class.java, "456") + val vs = repository.read(ValueSet::class.java, id) + + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + } + + @Test + fun searchValueSet() { + val sets = repository.search(Bundle::class.java, ValueSet::class.java, Searches.byUrl("example.com/ValueSet/456")) + assertNotNull(sets) + assertEquals(1, sets.entry.size) + } + + @Test + fun searchWithExternalValueSet() { + val sets = repository.search(Bundle::class.java, ValueSet::class.java, Searches.ALL) + assertNotNull(sets) + assertEquals(2, sets.entry.size) + } + + @Test + fun createAndDeleteLibrary() { + val lib = Library() + lib.id = "new-library" + val o = repository.create(lib) + val created = repository.read(Library::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("resources/library/new-library.json") + assertTrue(Files.exists(loc)) + + repository.delete(Library::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeletePatient() { + val p = Patient() + p.id = "new-patient" + val o = repository.create(p) + val created = repository.read(Patient::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("tests/patient/new-patient.json") + assertTrue(Files.exists(loc)) + + repository.delete(Patient::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeleteValueSet() { + val v = ValueSet() + v.id = "new-valueset" + val o = repository.create(v) + val created = repository.read(ValueSet::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("vocabulary/valueset/new-valueset.json") + assertTrue(Files.exists(loc)) + + repository.delete(ValueSet::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun deleteNonExistentPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.delete(Patient::class.java, id) } + } + + @Test + fun searchNonExistentType() { + val results = repository.search(Bundle::class.java, Encounter::class.java, Searches.ALL) + assertNotNull(results) + assertEquals(0, results.entry.size) + } + + @Test + fun readExternalValueSet() { + val id: IIdType = Ids.newId(ValueSet::class.java, "789") + val vs = repository.read(ValueSet::class.java, id) + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + + // Should be tagged with its source path + val path = vs.getUserData(IgStandardRepository.SOURCE_PATH_TAG) as Path + assertNotNull(path) + assertTrue(path.toFile().exists()) + assertTrue(path.toString().contains("external")) + } + + @Test + fun searchExternalValueSet() { + val sets = repository.search(Bundle::class.java, ValueSet::class.java, Searches.byUrl("example.com/ValueSet/789")) + assertNotNull(sets) + assertEquals(1, sets.entry.size) + } + + @Test + fun updateExternalValueSetFails() { + val id: IIdType = Ids.newId(ValueSet::class.java, "789") + val vs = repository.read(ValueSet::class.java, id) + assertThrows(ForbiddenOperationException::class.java) { repository.update(vs) } + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.kt new file mode 100644 index 00000000..07d382c8 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/MultiMeasureTest.kt @@ -0,0 +1,127 @@ +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.instance.model.api.IIdType +import org.hl7.fhir.r4.model.Library +import org.hl7.fhir.r4.model.Patient +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.opencds.cqf.fhir.test.Resources +import org.opencds.cqf.fhir.utility.Ids +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.util.stream.Collectors + +class MultiMeasureTest { + companion object { + private val log: Logger = LoggerFactory.getLogger(MultiMeasureTest::class.java) + + private const val ROOT_DIR = "/sampleIgs/ig/standard/cqlMeasures/multiMeasure" + private const val MODEL_PATH_MEASURE_100_TEST_CASE_1111 = "input/tests/measure/measure100/1111" + private const val MODEL_PATH_MEASURE_100_TEST_CASE_2222 = "input/tests/measure/measure100/2222" + private const val MODEL_PATH_MEASURE_200_TEST_CASE_1111 = "input/tests/measure/measure200/1111" + private const val TERMINOLOGY_PATH = "input/vocabulary/valueset" + + @TempDir + @JvmField + var tempDir: Path? = null + + @TempDir + @JvmField + var pathModelPathMeasure100TestCase1111TempDir: Path? = null + + @TempDir + @JvmField + var pathModelPathMeasure100TestCase2222TempDir: Path? = null + + @TempDir + @JvmField + var pathModelPathMeasure200TestCase1111TempDir: Path? = null + + @TempDir + @JvmField + var pathTerminologyTempDir: Path? = null + + lateinit var model1111Measure100Repo: IRepository + lateinit var model2222Measure100Repo: IRepository + lateinit var model1111Measure200Repo: IRepository + lateinit var terminologyRepo: IRepository + + fun listFiles(path: Path?) { + if (path == null) return + val pathExists = path.toFile().exists() + log.info("path[{}] exists: {}", path, pathExists) + if (pathExists) { + try { + Files.walk(path).use { stream -> + val fileNames = stream.map { it.toString() }.collect(Collectors.joining("\n ")) + log.info("resources: \n {}", fileNames) + } + } catch (e: IOException) { + log.error("Exception while capturing filenames. {}", e.message) + } + } + } + + @BeforeAll + @JvmStatic + fun setup() { + // This copies the sample IG to a temporary directory so that + // we can test against an actual filesystem + Resources.copyFromJar(ROOT_DIR, tempDir!!) + + val pathMeasure100Case1111 = tempDir!!.resolve(MODEL_PATH_MEASURE_100_TEST_CASE_1111) + val pathMeasure100Case2222 = tempDir!!.resolve(MODEL_PATH_MEASURE_100_TEST_CASE_2222) + val pathMeasure200Case1111 = tempDir!!.resolve(MODEL_PATH_MEASURE_200_TEST_CASE_1111) + val pathTerminology = tempDir!!.resolve(TERMINOLOGY_PATH) + + model1111Measure100Repo = IgStandardRepository(FhirContext.forR4Cached(), pathMeasure100Case1111) + model2222Measure100Repo = IgStandardRepository(FhirContext.forR4Cached(), pathMeasure100Case2222) + model1111Measure200Repo = IgStandardRepository(FhirContext.forR4Cached(), pathMeasure200Case1111) + terminologyRepo = IgStandardRepository(FhirContext.forR4Cached(), pathTerminology) + + listFiles(tempDir) + listFiles(pathMeasure100Case1111) + listFiles(pathMeasure100Case2222) + listFiles(pathMeasure200Case1111) + listFiles(pathTerminology) + } + } + + @Test + fun should_throwException_when_libraryDoesNotExist() { + val id: IIdType = Ids.newId(Library::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { model1111Measure100Repo.read(Library::class.java, id) } + assertThrows(ResourceNotFoundException::class.java) { model2222Measure100Repo.read(Library::class.java, id) } + assertThrows(ResourceNotFoundException::class.java) { model1111Measure200Repo.read(Library::class.java, id) } + assertThrows(ResourceNotFoundException::class.java) { terminologyRepo.read(Library::class.java, id) } + } + + // Test works locally but doesn't work on GitHub + // @Disabled("Disabled until issue with running test on github is resolved.") + @Test + fun should_findResourceInCorrectRepo_when_resourcesIsolatedByRepo() { + val id: IIdType = Ids.newId(Patient::class.java, "1111") + val patientFrommModel1111Measure100Repo = model1111Measure100Repo.read(Patient::class.java, id) + val patientFrommModel1111Measure200Repo = model1111Measure200Repo.read(Patient::class.java, id) + + assertEquals( + id.idPart, + patientFrommModel1111Measure100Repo.idElement.idPart, + ) + assertEquals( + id.idPart, + patientFrommModel1111Measure200Repo.idElement.idPart, + ) + assertNotEquals(patientFrommModel1111Measure100Repo.name, patientFrommModel1111Measure200Repo.name) + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.kt new file mode 100644 index 00000000..c3cd00c8 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/NoTestDataTest.kt @@ -0,0 +1,153 @@ +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.instance.model.api.IIdType +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.CodeSystem +import org.hl7.fhir.r4.model.Condition +import org.hl7.fhir.r4.model.Encounter +import org.hl7.fhir.r4.model.Library +import org.hl7.fhir.r4.model.Measure +import org.hl7.fhir.r4.model.Patient +import org.hl7.fhir.r4.model.ValueSet +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +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 +import java.nio.file.Files +import java.nio.file.Path + +/** + * This set of tests ensures that we can create new directories as needed if + * they don't exist ahead of time + */ +class NoTestDataTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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 = IgStandardRepository(FhirContext.forR4Cached(), tempDir!!) + } + } + + @Test + fun createAndDeleteLibrary() { + val lib = Library() + lib.id = "new-library" + val o = repository.create(lib) + val created = repository.read(Library::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("resources/library/new-library.json") + assertTrue(Files.exists(loc)) + + repository.delete(Library::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeleteMeasure() { + val measure = Measure() + measure.id = "new-measure" + val o = repository.create(measure) + val created = repository.read(Measure::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("resources/measure/new-measure.json") + assertTrue(Files.exists(loc)) + + repository.delete(Measure::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeletePatient() { + val p = Patient() + p.id = "new-patient" + val o = repository.create(p) + val created = repository.read(Patient::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("tests/patient/new-patient.json") + assertTrue(Files.exists(loc)) + + repository.delete(Patient::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeleteCondition() { + val p = Condition() + p.id = "new-condition" + val o = repository.create(p) + val created = repository.read(Condition::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("tests/condition/new-condition.json") + assertTrue(Files.exists(loc)) + + repository.delete(Condition::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeleteValueSet() { + val v = ValueSet() + v.id = "new-valueset" + val o = repository.create(v) + val created = repository.read(ValueSet::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("vocabulary/valueset/new-valueset.json") + assertTrue(Files.exists(loc)) + + repository.delete(ValueSet::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeleteCodeSystem() { + val c = CodeSystem() + c.id = "new-codesystem" + val o = repository.create(c) + val created = repository.read(CodeSystem::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("vocabulary/codesystem/new-codesystem.json") + assertTrue(Files.exists(loc)) + + repository.delete(CodeSystem::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun deleteNonExistentPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.delete(Patient::class.java, id) } + } + + @Test + fun searchNonExistentType() { + val results = repository.search(Bundle::class.java, Encounter::class.java, Searches.ALL) + assertNotNull(results) + assertEquals(0, results.entry.size) + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/OverwriteEncodingTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/OverwriteEncodingTest.kt new file mode 100644 index 00000000..172c0d7e --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/OverwriteEncodingTest.kt @@ -0,0 +1,69 @@ +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.api.EncodingEnum +import org.hl7.fhir.instance.model.api.IIdType +import org.hl7.fhir.r4.model.Library +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +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.nio.file.Path + +class OverwriteEncodingTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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!!) + val conventions = IgStandardConventions.autoDetect(tempDir!!) + repository = + IgStandardRepository( + FhirContext.forR4Cached(), + tempDir!!, + conventions, + IgStandardEncodingBehavior( + EncodingEnum.XML, + IgStandardEncodingBehavior.PreserveEncoding.OVERWRITE_WITH_PREFERRED_ENCODING, + ), + null, + ) + } + } + + @Test + fun readLibrary() { + val id: IIdType = Ids.newId(Library::class.java, "123") + val lib = repository.read(Library::class.java, id) + assertNotNull(lib) + assertEquals(id.idPart, lib.idElement.idPart) + } + + @Test + fun updateLibrary() { + val id: IIdType = Ids.newId(Library::class.java, "123") + val lib = repository.read(Library::class.java, id) + assertNotNull(lib) + assertEquals(id.idPart, lib.idElement.idPart) + + lib.addAuthor().name = "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/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/PrefixTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/PrefixTest.kt new file mode 100644 index 00000000..f23db2cd --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/PrefixTest.kt @@ -0,0 +1,239 @@ +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.instance.model.api.IIdType +import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.Condition +import org.hl7.fhir.r4.model.Encounter +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.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +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 +import java.nio.file.Files +import java.nio.file.Path + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class PrefixTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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 = IgStandardRepository(FhirContext.forR4Cached(), tempDir!!) + } + } + + @Test + fun readLibrary() { + val id: IIdType = Ids.newId(Library::class.java, "123") + val lib = repository.read(Library::class.java, id) + assertNotNull(lib) + assertEquals(id.idPart, lib.idElement.idPart) + } + + @Test + fun readLibraryNotExists() { + val id: IIdType = Ids.newId(Library::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.read(Library::class.java, id) } + } + + @Test + fun searchLibrary() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.ALL) + + assertNotNull(libs) + assertEquals(2, libs.entry.size) + } + + @Test + fun searchLibraryWithFilter() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("http://example.com/Library/Test")) + + assertNotNull(libs) + assertEquals(1, libs.entry.size) + } + + @Test + fun searchLibraryNotExists() { + val libs = repository.search(Bundle::class.java, Library::class.java, Searches.byUrl("not-exists")) + assertNotNull(libs) + assertEquals(0, libs.entry.size) + } + + @Test + fun readPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "ABC") + val cond = repository.read(Patient::class.java, id) + + assertNotNull(cond) + assertEquals(id.idPart, cond.idElement.idPart) + } + + @Test + fun searchCondition() { + val cons = + repository.search( + Bundle::class.java, + Condition::class.java, + Searches.byCodeAndSystem("12345", "example.com/codesystem"), + ) + assertNotNull(cons) + assertEquals(2, cons.entry.size) + } + + @Test + fun readValueSet() { + val id: IIdType = Ids.newId(ValueSet::class.java, "456") + val vs = repository.read(ValueSet::class.java, id) + + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + } + + @Test + fun searchValueSet() { + val sets = repository.search(Bundle::class.java, ValueSet::class.java, Searches.byUrl("example.com/ValueSet/456")) + assertNotNull(sets) + assertEquals(1, sets.entry.size) + } + + @Test + fun createAndDeleteLibrary() { + val lib = Library() + lib.id = "new-library" + val o = repository.create(lib) + val created = repository.read(Library::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("resources/library/Library-new-library.json") + assertTrue(Files.exists(loc)) + + repository.delete(Library::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeletePatient() { + val p = Patient() + p.id = "new-patient" + val o = repository.create(p) + val created = repository.read(Patient::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("tests/patient/Patient-new-patient.json") + assertTrue(Files.exists(loc)) + + repository.delete(Patient::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeleteValueSet() { + val v = ValueSet() + v.id = "new-valueset" + val o = repository.create(v) + val created = repository.read(ValueSet::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("vocabulary/valueset/ValueSet-new-valueset.json") + assertTrue(Files.exists(loc)) + + repository.delete(ValueSet::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun updatePatient() { + val id: IIdType = Ids.newId(Patient::class.java, "ABC") + val p = repository.read(Patient::class.java, id) + assertFalse(p.hasActive()) + + p.active = true + repository.update(p) + + val updated = repository.read(Patient::class.java, id) + assertTrue(updated.hasActive()) + assertTrue(updated.active) + } + + @Test + fun deleteNonExistentPatient() { + val id: IIdType = Ids.newId(Patient::class.java, "DoesNotExist") + assertThrows(ResourceNotFoundException::class.java) { repository.delete(Patient::class.java, id) } + } + + @Test + fun searchNonExistentType() { + val results = repository.search(Bundle::class.java, Encounter::class.java, Searches.ALL) + assertNotNull(results) + assertEquals(0, results.entry.size) + } + + @Test + fun searchById() { + val bundle = repository.search(Bundle::class.java, Library::class.java, Searches.byId("123")) + assertNotNull(bundle) + assertEquals(1, bundle.entry.size) + } + + @Test + fun searchByIdNotFound() { + val bundle = repository.search(Bundle::class.java, Library::class.java, Searches.byId("DoesNotExist")) + assertNotNull(bundle) + assertEquals(0, bundle.entry.size) + } + + @Test + @Order(1) // Do this test first because it puts the filesystem (temporarily) in an invalid state + fun resourceMissingWhenCacheCleared() { + val id = org.hl7.fhir.r4.model.IdType("Library", "ToDelete") + var lib = Library().setIdElement(id) + val path = tempDir!!.resolve("resources/library/Library-ToDelete.json") + + repository.create(lib) + assertTrue(path.toFile().exists()) + + // Read back, should exist + lib = repository.read(Library::class.java, 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.java, id) + assertNotNull(lib) + assertEquals("ToDelete", lib.idElement.idPart) + + (repository as IgStandardRepository).clearCache() + + // Try to read again, should be gone because it's not in the cache and the content is gone. + assertThrows(ResourceNotFoundException::class.java) { repository.read(Library::class.java, id) } + + // Clean up so that we don't affect other tests + path.toFile().delete() + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/XmlWriteTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/XmlWriteTest.kt new file mode 100644 index 00000000..2b1000a5 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/repository/ig/standard/XmlWriteTest.kt @@ -0,0 +1,103 @@ +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.api.EncodingEnum +import org.hl7.fhir.instance.model.api.IIdType +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.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +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 +import java.nio.file.Files +import java.nio.file.Path + +class XmlWriteTest { + companion object { + private lateinit var repository: IRepository + + @TempDir + @JvmField + var tempDir: Path? = null + + @BeforeAll + @JvmStatic + fun setup() { + // 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!!) + val conventions = IgStandardConventions.autoDetect(tempDir!!) + repository = + IgStandardRepository( + FhirContext.forR4Cached(), + tempDir!!, + conventions, + IgStandardEncodingBehavior( + EncodingEnum.XML, + IgStandardEncodingBehavior.PreserveEncoding.PRESERVE_ORIGINAL_ENCODING, + ), + null, + ) + } + } + + @Test + fun createAndDeleteLibrary() { + val lib = Library() + lib.id = "new-library" + val o = repository.create(lib) + val created = repository.read(Library::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("resources/library/new-library.xml") + assertTrue(Files.exists(loc)) + + repository.delete(Library::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun createAndDeletePatient() { + val p = Patient() + p.id = "new-patient" + val o = repository.create(p) + val created = repository.read(Patient::class.java, o.id!!) + assertNotNull(created) + + val loc = tempDir!!.resolve("tests/patient/new-patient.xml") + assertTrue(Files.exists(loc)) + + repository.delete(Patient::class.java, created.idElement) + assertFalse(Files.exists(loc)) + } + + @Test + fun readExternalValueSet() { + val id: IIdType = Ids.newId(ValueSet::class.java, "789") + val vs = repository.read(ValueSet::class.java, id) + assertNotNull(vs) + assertEquals(vs.idElement.idPart, vs.idElement.idPart) + + // Should be tagged with its source path + val path = vs.getUserData(IgStandardRepository.SOURCE_PATH_TAG) as Path + assertNotNull(path) + assertTrue(path.toFile().exists()) + assertTrue(path.toString().contains("external")) + } + + @Test + fun searchExternalValueSet() { + val sets = repository.search(Bundle::class.java, ValueSet::class.java, Searches.byUrl("example.com/ValueSet/789")) + assertNotNull(sets) + assertEquals(1, sets.entry.size) + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.kt new file mode 100644 index 00000000..a812eb40 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.kt @@ -0,0 +1,178 @@ +package org.opencds.cqf.cql.ls.server.service + +import org.eclipse.lsp4j.DidChangeTextDocumentParams +import org.eclipse.lsp4j.DidCloseTextDocumentParams +import org.eclipse.lsp4j.DidOpenTextDocumentParams +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentContentChangeEvent +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.TextDocumentItem +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent +import java.net.URI + +class ActiveContentServiceTest { + private fun openDoc( + text: String, + version: Int, + ): ActiveContentService { + val svc = ActiveContentService() + val params = DidOpenTextDocumentParams() + val item = TextDocumentItem() + item.uri = DOC_URI + item.text = text + item.version = version + params.textDocument = item + svc.didOpen(DidOpenTextDocumentEvent(params)) + return svc + } + + // ----------------------------------------------------------------------- + // didOpen / didClose + // ----------------------------------------------------------------------- + + @Test + fun didOpen_addsUriToActiveSet() { + val svc = openDoc(LIBRARY_CONTENT, 1) + assertTrue(svc.activeUris().contains(DOC_URI_PARSED)) + } + + @Test + fun didClose_removesUriFromActiveSet() { + val svc = openDoc(LIBRARY_CONTENT, 1) + + val close = DidCloseTextDocumentParams() + close.textDocument = TextDocumentIdentifier(DOC_URI) + svc.didClose(DidCloseTextDocumentEvent(close)) + + assertFalse(svc.activeUris().contains(DOC_URI_PARSED)) + } + + // ----------------------------------------------------------------------- + // didChange + // ----------------------------------------------------------------------- + + @Test + fun didChange_fullReplacement_updatesContent() { + val svc = openDoc("old content", 1) + + val change = DidChangeTextDocumentParams() + change.textDocument = VersionedTextDocumentIdentifier(DOC_URI, 2) + change.contentChanges = listOf(TextDocumentContentChangeEvent("new content")) + svc.didChange(DidChangeTextDocumentEvent(change)) + + val updated = svc.read(DOC_URI_PARSED)!!.readAllBytes().toString(Charsets.UTF_8) + assertEquals("new content", updated) + } + + @Test + fun didChange_olderVersion_contentUnchanged() { + val svc = openDoc("original", 5) + + val change = DidChangeTextDocumentParams() + // version 3 < current version 5, so change should be ignored + change.textDocument = VersionedTextDocumentIdentifier(DOC_URI, 3) + change.contentChanges = listOf(TextDocumentContentChangeEvent("should be ignored")) + svc.didChange(DidChangeTextDocumentEvent(change)) + + val content = svc.read(DOC_URI_PARSED)!!.readAllBytes().toString(Charsets.UTF_8) + assertEquals("original", content) + } + + // ----------------------------------------------------------------------- + // patch + // ----------------------------------------------------------------------- + + @Test + @Suppress("DEPRECATION") + fun patch_singleLineReplacement_replacesCorrectRange() { + val svc = ActiveContentService() + + val change = TextDocumentContentChangeEvent() + change.range = Range(Position(0, 6), Position(0, 11)) + change.text = "CQL" + change.rangeLength = 5 + + val result = svc.patch("hello world", change) + + assertEquals("hello CQL", result) + } + + @Test + @Suppress("DEPRECATION") + fun patch_multiLineReplacement_replacesAcrossLines() { + val svc = ActiveContentService() + + // Source: "line1\nline2\nline3" + // Replace from (0,5) to end of "line2": replacement = " and " + // rangeLength = 6 chars ("\nline2") + val change = TextDocumentContentChangeEvent() + change.range = Range(Position(0, 5), Position(1, 5)) + change.text = " and " + change.rangeLength = 6 + + val result = svc.patch("line1\nline2\nline3", change) + + assertEquals("line1 and \nline3", result) + } + + // ----------------------------------------------------------------------- + // searchActiveContent + // ----------------------------------------------------------------------- + + @Test + fun searchActiveContent_matchesNameAndVersion() { + val svc = openDoc(LIBRARY_CONTENT, 1) + + val id = VersionedIdentifier().withId("One").withVersion("1.0.0") + val found = svc.searchActiveContent(ROOT, id) + + assertTrue(found.contains(DOC_URI_PARSED)) + } + + @Test + fun searchActiveContent_uriOutsideRoot_excluded() { + val otherUri = "file:///other/One.cql" + val svc = ActiveContentService() + val params = DidOpenTextDocumentParams() + val item = TextDocumentItem() + item.uri = otherUri + item.text = LIBRARY_CONTENT + item.version = 1 + params.textDocument = item + svc.didOpen(DidOpenTextDocumentEvent(params)) + + val id = VersionedIdentifier().withId("One").withVersion("1.0.0") + val found = svc.searchActiveContent(ROOT, id) + + assertFalse(found.contains(Uris.parseOrNull(otherUri))) + } + + @Test + fun searchActiveContent_contentDoesNotMatch_notIncluded() { + val svc = openDoc("library Two version '2.0.0'\n\ndefine \"X\": 1", 1) + + val id = VersionedIdentifier().withId("One").withVersion("1.0.0") + val found = svc.searchActiveContent(ROOT, id) + + assertNotNull(found) + assertFalse(found.contains(DOC_URI_PARSED)) + } + + companion object { + private const val DOC_URI = "file:///workspace/One.cql" + private val DOC_URI_PARSED: URI = Uris.parseOrNull(DOC_URI)!! + private val ROOT: URI = Uris.parseOrNull("file:///workspace/")!! + private const val LIBRARY_CONTENT = "library One version '1.0.0'\n\ndefine \"Test\":\n 1\n" + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt new file mode 100644 index 00000000..ade42beb --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt @@ -0,0 +1,84 @@ +package org.opencds.cqf.cql.ls.server.service + +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import org.opencds.cqf.cql.ls.core.ContentService +import java.io.ByteArrayInputStream +import java.net.URI + +class FederatedContentServiceTest { + private lateinit var activeService: ActiveContentService + private lateinit var fileService: ContentService + private lateinit var fedService: FederatedContentService + + @BeforeEach + fun setUp() { + activeService = mock(ActiveContentService::class.java) + fileService = mock(ContentService::class.java) + fedService = FederatedContentService(activeService, fileService) + } + + // ----------------------------------------------------------------------- + // locate — merges results from both services + // ----------------------------------------------------------------------- + + @Test + fun locate_mergesActiveAndFileResults() { + val id = VersionedIdentifier().withId("One").withVersion("1.0.0") + `when`(activeService.locate(ROOT, id)).thenReturn(mutableSetOf(ACTIVE_URI)) + `when`(fileService.locate(ROOT, id)).thenReturn(mutableSetOf(FILE_URI)) + + val result = fedService.locate(ROOT, id) + + assertTrue(result.contains(ACTIVE_URI)) + assertTrue(result.contains(FILE_URI)) + } + + @Test + fun locate_emptyFromBoth_returnsEmptySet() { + val id = VersionedIdentifier().withId("Unknown") + `when`(activeService.locate(ROOT, id)).thenReturn(mutableSetOf()) + `when`(fileService.locate(ROOT, id)).thenReturn(mutableSetOf()) + + val result = fedService.locate(ROOT, id) + + assertTrue(result.isEmpty()) + } + + // ----------------------------------------------------------------------- + // read — prefers active content when URI is in active set + // ----------------------------------------------------------------------- + + @Test + fun read_uriInActiveSet_returnsActiveStream() { + val expected = ByteArrayInputStream("active content".toByteArray()) + `when`(activeService.activeUris()).thenReturn(setOf(ACTIVE_URI)) + `when`(activeService.read(ACTIVE_URI)).thenReturn(expected) + + val result = fedService.read(ACTIVE_URI) + + assertSame(expected, result) + } + + @Test + fun read_uriNotInActiveSet_fallsBackToFileService() { + val expected = ByteArrayInputStream("file content".toByteArray()) + `when`(activeService.activeUris()).thenReturn(emptySet()) + `when`(fileService.read(FILE_URI)).thenReturn(expected) + + val result = fedService.read(FILE_URI) + + assertSame(expected, result) + } + + companion object { + private val ROOT: URI = URI.create("file:///workspace/") + private val ACTIVE_URI: URI = URI.create("file:///workspace/One.cql") + private val FILE_URI: URI = URI.create("file:///workspace/lib/One.cql") + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FileContentServiceTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FileContentServiceTest.kt new file mode 100644 index 00000000..538befbc --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FileContentServiceTest.kt @@ -0,0 +1,132 @@ +package org.opencds.cqf.cql.ls.server.service + +import org.eclipse.lsp4j.WorkspaceFolder +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.net.URI + +class FileContentServiceTest { + @TempDir + lateinit var tempDir: File + + private fun id( + name: String, + version: String, + ) = VersionedIdentifier().withId(name).withVersion(version) + + private fun id(name: String) = VersionedIdentifier().withId(name) + + // ----------------------------------------------------------------------- + // searchFolder (static) + // ----------------------------------------------------------------------- + + @Test + fun searchFolder_exactVersionedMatch_returnsFile() { + val cqlFile = File(tempDir, "FHIRHelpers-4.0.1.cql").also { it.createNewFile() } + + val result = FileContentService.searchFolder(tempDir.toURI(), id("FHIRHelpers", "4.0.1")) + + assertNotNull(result) + assertEquals(cqlFile.canonicalPath, result!!.canonicalPath) + } + + @Test + fun searchFolder_unversionedFilename_treatedAsNearestMatch() { + // A file named "FHIRHelpers.cql" (no version suffix) is returned when + // the versioned file does not exist, as it is treated as the most-recent version. + val cqlFile = File(tempDir, "FHIRHelpers.cql").also { it.createNewFile() } + + val result = FileContentService.searchFolder(tempDir.toURI(), id("FHIRHelpers", "4.0.1")) + + assertNotNull(result) + assertEquals(cqlFile.canonicalPath, result!!.canonicalPath) + } + + @Test + fun searchFolder_noMatch_returnsNull() { + val result = FileContentService.searchFolder(tempDir.toURI(), id("NonExistent", "1.0.0")) + assertNull(result) + } + + @Test + fun searchFolder_compatibleVersion_returnsNearest() { + // 4.0.1 is compatible with a request for 4.0.0 (same major/minor, higher patch). + File(tempDir, "FHIRHelpers-4.0.1.cql").createNewFile() + + val result = FileContentService.searchFolder(tempDir.toURI(), id("FHIRHelpers", "4.0.0")) + + assertNotNull(result) + } + + @Test + fun searchFolder_noMatchingPrefix_returnsNull() { + // "SomeOtherLib.cql" does not start with "MyLib", so the filter never returns it. + File(tempDir, "SomeOtherLib.cql").createNewFile() + + val result = FileContentService.searchFolder(tempDir.toURI(), id("MyLib", "1.0.0")) + + assertNull(result) + } + + // ----------------------------------------------------------------------- + // read(URI) + // ----------------------------------------------------------------------- + + @Test + fun read_existingFile_returnsStream() { + val cqlFile = File(tempDir, "Test.cql").also { it.createNewFile() } + + val svc = FileContentService(emptyList()) + val stream = svc.read(cqlFile.toURI()) + + assertNotNull(stream) + stream!!.close() + } + + @Test + fun read_nonExistentFile_returnsNull() { + val svc = FileContentService(emptyList()) + val uri: URI = File(tempDir, "does-not-exist.cql").toURI() + + assertNull(svc.read(uri)) + } + + // ----------------------------------------------------------------------- + // locate(URI, VersionedIdentifier) + // ----------------------------------------------------------------------- + + @Test + fun locate_rootWithinWorkspace_returnsUri() { + File(tempDir, "One.cql").createNewFile() + + val folder = WorkspaceFolder() + folder.uri = tempDir.toURI().toString() + val svc = FileContentService(listOf(folder)) + + val result = svc.locate(tempDir.toURI(), id("One")) + + assertNotNull(result) + assertTrue(result.size >= 1) + } + + @Test + fun locate_rootOutsideWorkspace_returnsEmpty() { + val workspace = File(tempDir, "workspace").also { it.mkdirs() } + File(workspace, "One.cql").createNewFile() + + val folder = WorkspaceFolder() + folder.uri = workspace.toURI().toString() + val svc = FileContentService(listOf(folder)) + + // root = tempDir, which is NOT inside the workspace subfolder + val result = svc.locate(tempDir.toURI(), id("One")) + + assertTrue(result.isEmpty()) + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/utility/DiagnosticsTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/utility/DiagnosticsTest.kt new file mode 100644 index 00000000..280d58df --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/utility/DiagnosticsTest.kt @@ -0,0 +1,130 @@ +package org.opencds.cqf.cql.ls.server.utility + +import org.cqframework.cql.cql2elm.CqlCompilerException +import org.cqframework.cql.cql2elm.CqlSemanticException +import org.cqframework.cql.cql2elm.tracking.TrackBack +import org.eclipse.lsp4j.DiagnosticSeverity +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class DiagnosticsTest { + // ----------------------------------------------------------------------- + // convert(CqlCompilerException) + // ----------------------------------------------------------------------- + + @Test + fun convert_withLocator_appliesIndexConversion() { + // TrackBack is 1-indexed; LSP is 0-indexed. + // startChar -1, endChar no -1. + val tb = locator(5, 3, 5, 15) + val d = Diagnostics.convert(exception("err", tb, CqlCompilerException.ErrorSeverity.Error)) + + assertNotNull(d) + assertEquals(4, d!!.range.start.line) + assertEquals(2, d.range.start.character) + assertEquals(4, d.range.end.line) + assertEquals(15, d.range.end.character) // end char: no -1 + } + + @Test + fun convert_withNullLocator_returnsNull() { + val error = CqlSemanticException("no locator", null, CqlCompilerException.ErrorSeverity.Error, null) + assertNull(Diagnostics.convert(error)) + } + + @Test + fun convert_line1Char1_clampedToZero() { + // Line 1, char 1 → line 0, char 0 (max(..., 0) prevents negative) + val tb = locator(1, 1, 1, 1) + val d = Diagnostics.convert(exception("msg", tb, CqlCompilerException.ErrorSeverity.Error)) + + assertNotNull(d) + assertEquals(0, d!!.range.start.line) + assertEquals(0, d.range.start.character) + assertEquals(0, d.range.end.line) + assertEquals(1, d.range.end.character) // endChar = 1 (no -1) + } + + @Test + fun convert_messagePropagated() { + val tb = locator(2, 1, 2, 5) + val d = Diagnostics.convert(exception("my error message", tb, CqlCompilerException.ErrorSeverity.Error)) + + assertEquals("my error message", d!!.message) + } + + // ----------------------------------------------------------------------- + // severity mapping + // ----------------------------------------------------------------------- + + @Test + fun severity_error_mapsToError() { + val tb = locator(1, 1, 1, 5) + val d = Diagnostics.convert(exception("msg", tb, CqlCompilerException.ErrorSeverity.Error)) + assertEquals(DiagnosticSeverity.Error, d!!.severity) + } + + @Test + fun severity_warning_mapsToWarning() { + val tb = locator(1, 1, 1, 5) + val d = Diagnostics.convert(exception("msg", tb, CqlCompilerException.ErrorSeverity.Warning)) + assertEquals(DiagnosticSeverity.Warning, d!!.severity) + } + + @Test + fun severity_info_mapsToInformation() { + val tb = locator(1, 1, 1, 5) + val d = Diagnostics.convert(exception("msg", tb, CqlCompilerException.ErrorSeverity.Info)) + assertEquals(DiagnosticSeverity.Information, d!!.severity) + } + + // ----------------------------------------------------------------------- + // convert(Iterable) + // ----------------------------------------------------------------------- + + @Test + fun convertIterable_emptyList_returnsEmptySet() { + val result = Diagnostics.convert(emptyList()) + assertNotNull(result) + assertTrue(result.isEmpty()) + } + + @Test + fun convertIterable_mixedLocators_onlyLocatedIncluded() { + val located = exception("located", locator(1, 1, 1, 10), CqlCompilerException.ErrorSeverity.Error) + val unlocated = CqlSemanticException("unlocated", null, CqlCompilerException.ErrorSeverity.Error, null) + + val result = Diagnostics.convert(listOf(located, unlocated)) + + assertEquals(1, result.size) + } + + @Test + fun convertIterable_twoLocatedErrors_bothIncluded() { + val e1 = exception("first", locator(1, 1, 1, 5), CqlCompilerException.ErrorSeverity.Error) + val e2 = exception("second", locator(2, 1, 2, 5), CqlCompilerException.ErrorSeverity.Error) + + val result = Diagnostics.convert(listOf(e1, e2)) + + assertEquals(2, result.size) + } + + companion object { + private fun locator( + startLine: Int, + startChar: Int, + endLine: Int, + endChar: Int, + ) = TrackBack(VersionedIdentifier(), startLine, startChar, endLine, endChar) + + private fun exception( + message: String, + tb: TrackBack, + severity: CqlCompilerException.ErrorSeverity, + ) = CqlSemanticException(message, tb, severity, null) + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.kt new file mode 100644 index 00000000..41eb425f --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.kt @@ -0,0 +1,82 @@ +package org.opencds.cqf.cql.ls.server.visitor + +import org.cqframework.cql.cql2elm.tracking.TrackBack +import org.hamcrest.CoreMatchers.instanceOf +import org.hamcrest.MatcherAssert.assertThat +import org.hl7.elm.r1.ExpressionDef +import org.hl7.elm.r1.Library +import org.hl7.elm.r1.Retrieve +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import org.opencds.cqf.cql.ls.server.service.TestContentService + +class ExpressionTrackBackVisitorTest { + companion object { + private lateinit var library: Library + + @BeforeAll + @JvmStatic + fun beforeAll() { + val cs = TestContentService() + val cqlCompilationManager = + CqlCompilationManager(cs, CompilerOptionsManager(cs), IgContextManager(cs)) + library = + cqlCompilationManager + .compile(Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitorTest.cql")!!)!! + .compiledLibrary!! + .library!! + } + } + + @Test + fun positionInRetrieve_returnsRetrieve() { + val visitor = ExpressionTrackBackVisitor() + val tb = TrackBack(library.identifier, 9, 9, 9, 9) + val e = visitor.visitLibrary(library, tb) + assertNotNull(e) + assertThat(e, instanceOf(Retrieve::class.java)) + } + + @Test + fun positionOutsideExpression_returnsNull() { + val visitor = ExpressionTrackBackVisitor() + val tb = TrackBack(library.identifier, 10, 0, 10, 0) + val e = visitor.visitLibrary(library, tb) + assertNull(e) + } + + @Test + fun positionInExpression_returnsExpressionDef() { + val visitor = ExpressionTrackBackVisitor() + val tb = TrackBack(library.identifier, 15, 10, 15, 10) + val e = visitor.visitLibrary(library, tb) + assertNotNull(e) + assertThat(e, instanceOf(ExpressionDef::class.java)) + } + + @Test + fun positionInExpressionRef_returnsExpressionDef() { + // Line 12 (1-indexed): " "ObservationRetrieve"" — the ExpressionRef inside ObservationReference + val visitor = ExpressionTrackBackVisitor() + val tb = TrackBack(library.identifier, 12, 5, 12, 25) + val e = visitor.visitLibrary(library, tb) + assertNotNull(e) + assertThat(e, instanceOf(ExpressionDef::class.java)) + } + + @Test + fun positionAtExpressionBoundary_returnsExpressionDef() { + // Line 15 (1-indexed): " Patient.birthDate" — at the start char of PropertyAccess body + val visitor = ExpressionTrackBackVisitor() + val tb = TrackBack(library.identifier, 15, 5, 15, 5) + val e = visitor.visitLibrary(library, tb) + assertNotNull(e) + assertThat(e, instanceOf(ExpressionDef::class.java)) + } +} diff --git a/ls/server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/ls/server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..1f0955d4 --- /dev/null +++ b/ls/server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/ls/service/pom.xml b/ls/service/pom.xml index 49e4a7b6..8a280109 100644 --- a/ls/service/pom.xml +++ b/ls/service/pom.xml @@ -21,41 +21,43 @@ cql-ls-server ${project.version} - - org.springframework.boot - spring-boot - - - org.springframework.boot - spring-boot-starter-logging - ch.qos.logback logback-classic + + org.slf4j + jul-to-slf4j + org.apache.maven.plugins - maven-jar-plugin - - - - org.opencds.cqf.cql.ls.service.Main - - - - - - org.springframework.boot - spring-boot-maven-plugin + maven-shade-plugin - - repackage - + package + shade + + + + org.opencds.cqf.cql.ls.service.MainKt + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + diff --git a/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/LanguageClientAppender.java b/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/LanguageClientAppender.java deleted file mode 100644 index 8a56e79e..00000000 --- a/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/LanguageClientAppender.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.opencds.cqf.cql.ls.service; - -import static com.google.common.base.Preconditions.checkNotNull; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.AppenderBase; -import org.eclipse.lsp4j.MessageParams; -import org.eclipse.lsp4j.MessageType; -import org.eclipse.lsp4j.services.LanguageClient; - -/** - * This class allows logging to the LSP client's {@code} logMessage} API - */ -public class LanguageClientAppender extends AppenderBase { - private final LanguageClient client; - - /** - * Constructs a LanguageClientAppender - * - * @param client the client to log to - */ - public LanguageClientAppender(LanguageClient client) { - this.client = client; - } - - @Override - protected void append(ILoggingEvent eventObject) { - if (eventObject == null) { - return; - } - - this.client.logMessage(createMessageParams(eventObject)); - } - - MessageParams createMessageParams(ILoggingEvent eventObject) { - checkNotNull(eventObject); - - return new MessageParams(toType(eventObject.getLevel()), eventObject.getFormattedMessage()); - } - - MessageType toType(Level level) { - if (level == Level.ERROR) { - return MessageType.Error; - } else if (level == Level.WARN) { - return MessageType.Warning; - } else if (level == Level.INFO) { - return MessageType.Info; - } else { - return MessageType.Log; - } - } -} diff --git a/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/LanguageClientAppender.kt b/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/LanguageClientAppender.kt new file mode 100644 index 00000000..f96d6dbb --- /dev/null +++ b/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/LanguageClientAppender.kt @@ -0,0 +1,26 @@ +package org.opencds.cqf.cql.ls.service + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import org.eclipse.lsp4j.MessageParams +import org.eclipse.lsp4j.MessageType +import org.eclipse.lsp4j.services.LanguageClient + +class LanguageClientAppender(private val client: LanguageClient) : AppenderBase() { + + override fun append(eventObject: ILoggingEvent?) { + if (eventObject == null) return + client.logMessage(createMessageParams(eventObject)) + } + + internal fun createMessageParams(eventObject: ILoggingEvent): MessageParams = + MessageParams(toType(eventObject.level), eventObject.formattedMessage) + + internal fun toType(level: Level): MessageType = when (level) { + Level.ERROR -> MessageType.Error + Level.WARN -> MessageType.Warning + Level.INFO -> MessageType.Info + else -> MessageType.Log + } +} diff --git a/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/Main.java b/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/Main.java deleted file mode 100644 index ba62e768..00000000 --- a/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/Main.java +++ /dev/null @@ -1,170 +0,0 @@ -package org.opencds.cqf.cql.ls.service; - -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.LoggerContext; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import org.cqframework.cql.cql2elm.CqlTranslator; -import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.eclipse.lsp4j.launch.LSPLauncher; -import org.eclipse.lsp4j.services.LanguageClient; -import org.opencds.cqf.cql.engine.execution.CqlEngine; -import org.opencds.cqf.cql.ls.server.CqlLanguageServer; -import org.opencds.cqf.cql.ls.server.config.ServerConfig; -import org.slf4j.LoggerFactory; -import org.slf4j.bridge.SLF4JBridgeHandler; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.SpringApplication; -import org.springframework.context.annotation.Import; - -/** - * Main entry point for the CQL Language Server service. - *

- * This class starts a CqlLanguageServer running as a service that communicates - * via the Language Server Protocol (LSP) over stdin/stdout. It is designed to be - * launched by LSP clients such as VS Code extensions. - *

- * Key behaviors: - *

    - *
  • Runs as a Spring Boot CommandLineRunner (non-web application)
  • - *
  • Logs to stderr to keep stdout clear for LSP communication
  • - *
  • Monitors both exit() notifications and connection closure for shutdown
  • - *
  • Terminates the JVM with System.exit(0) when the connection closes
  • - *
- */ -@Import(ServerConfig.class) -public class Main implements CommandLineRunner { - - private static final Logger log = (Logger) LoggerFactory.getLogger(Main.class); - - /** - * Entrypoint for the cql-ls-service - * - * @param args the command-line parameters (none supported currently) - */ - public static void main(String[] args) { - configureLogging(); - SpringApplication.run(Main.class, args); - } - - @Autowired - CqlLanguageServer server; - - @Override - public void run(String... args) { - @SuppressWarnings("java:S106") - Launcher launcher = LSPLauncher.createServerLauncher(server, System.in, System.out); - - LanguageClient client = launcher.getRemoteProxy(); - - // Logging Strategy: - // Currently logs are written to stderr (configured in logback.xml). - // The client can capture stderr and display it separately. - // - // Alternative: Send logs to the client's LSP window/logMessage API - // by uncommenting the line below. This would show logs in VS Code's - // Output panel for the language server channel. - // - // Trade-offs: - // - stderr: Simpler, works with any LSP client, easier to redirect to files - // - client API: Integrated into IDE, automatic log level filtering in UI - // - // setupClientAppender(client); - - server.connect(client); - Future serverThread = launcher.startListening(); - - log.info("java.version: {}", System.getProperty("java.version")); - log.info( - "cql-language-server version: {}", - CqlLanguageServer.class.getPackage().getImplementationVersion()); - log.info("cql-translator version: {}", CqlTranslator.class.getPackage().getImplementationVersion()); - log.info("cql-engine version: {}", CqlEngine.class.getPackage().getImplementationVersion()); - - log.info("cql-language-server started"); - - // Shutdown Strategy: - // Monitor two conditions in parallel to handle both normal and abnormal shutdowns: - // 1. server.exited() - Completes when LSP exit() notification is received (normal shutdown) - // 2. connectionClosed - Completes when stdin/stdout closes (client disconnect/crash) - // - // We wait for whichever happens first. This ensures the server terminates properly - // when VS Code closes, even if the exit() notification never arrives. - - ExecutorService executor = Executors.newSingleThreadExecutor(); - CompletableFuture connectionClosed = CompletableFuture.runAsync( - () -> { - try { - // This blocks until the LSP connection closes (stdin/stdout closed) - serverThread.get(); - log.info("LSP connection closed"); - } catch (Exception e) { - log.debug("Server thread exception", e); - } - }, - executor); - - // Wait for whichever completes first: exit() or connection closure - try { - CompletableFuture.anyOf(server.exited(), connectionClosed).get(); - } catch (Exception e) { - log.error("Error waiting for shutdown", e); - } - - log.info("Shutting down language server"); - serverThread.cancel(true); - executor.shutdownNow(); - - // Force JVM termination to ensure all threads (including Spring-managed ones) are stopped. - // Using System.exit(0) is necessary because Spring Boot may have non-daemon threads running. - System.exit(0); - } - - /** - * Configures SLF4J logging bridge to redirect java.util.logging to SLF4J. - * This ensures all logging from third-party libraries flows through our - * configured Logback appenders (currently configured to write to stderr). - */ - public static void configureLogging() { - SLF4JBridgeHandler.removeHandlersForRootLogger(); - SLF4JBridgeHandler.install(); - } - - /** - * Alternative logging approach: Send logs to the LSP client via window/logMessage. - *

- * When enabled, this appender sends all log messages to the language client's - * logMessage API, which displays them in the IDE's Output panel for the - * language server channel. - *

- * Benefits: - *

    - *
  • Logs appear directly in VS Code's Output panel
  • - *
  • IDE provides UI controls for filtering log levels
  • - *
  • No need to configure separate stderr capture
  • - *
- *

- * Drawbacks: - *

    - *
  • Logs can't be easily redirected to files
  • - *
  • May clutter the client connection with high-volume logging
  • - *
  • Dependent on client implementation of window/logMessage
  • - *
- *

- * To enable: Uncomment the setupClientAppender(client) call on line 63. - * - * @param client The LSP language client to send log messages to - */ - private static void setupClientAppender(LanguageClient client) { - LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); - LanguageClientAppender appender = new LanguageClientAppender(client); - appender.setContext(lc); - appender.start(); - - Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); - root.addAppender(appender); - } -} diff --git a/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/main.kt b/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/main.kt new file mode 100644 index 00000000..734cd77b --- /dev/null +++ b/ls/service/src/main/java/org/opencds/cqf/cql/ls/service/main.kt @@ -0,0 +1,114 @@ +package org.opencds.cqf.cql.ls.service + +import org.cqframework.cql.cql2elm.CqlTranslator +import org.eclipse.lsp4j.WorkspaceFolder +import org.eclipse.lsp4j.launch.LSPLauncher +import org.eclipse.lsp4j.services.LanguageClient +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Logger.JavaLogger +import org.opencds.cqf.cql.engine.execution.CqlEngine +import org.opencds.cqf.cql.ls.server.CqlLanguageServer +import org.opencds.cqf.cql.ls.server.command.DebugCqlCommandContribution +import org.opencds.cqf.cql.ls.server.command.ViewElmCommandContribution +import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import org.opencds.cqf.cql.ls.server.plugin.CommandContribution +import org.opencds.cqf.cql.ls.server.plugin.CqlLanguageServerPluginFactory +import org.opencds.cqf.cql.ls.server.provider.FormattingProvider +import org.opencds.cqf.cql.ls.server.provider.HoverProvider +import org.opencds.cqf.cql.ls.server.service.ActiveContentService +import org.opencds.cqf.cql.ls.server.service.CqlTextDocumentService +import org.opencds.cqf.cql.ls.server.service.CqlWorkspaceService +import org.opencds.cqf.cql.ls.server.service.DiagnosticsService +import org.opencds.cqf.cql.ls.server.service.FederatedContentService +import org.opencds.cqf.cql.ls.server.service.FileContentService +import org.slf4j.LoggerFactory +import org.slf4j.bridge.SLF4JBridgeHandler +import java.util.ServiceLoader +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors + +private val log = LoggerFactory.getLogger("org.opencds.cqf.cql.ls.service.Main") + +fun main(args: Array) { + configureLogging() + + val eventBus = EventBus.builder().logger(JavaLogger("eventBus")).build() + val workspaceFolders = mutableListOf() + + val activeContentService = ActiveContentService().also { eventBus.register(it) } + val fileContentService = FileContentService(workspaceFolders) + val federatedContentService = FederatedContentService(activeContentService, fileContentService) + + val compilerOptionsManager = CompilerOptionsManager(federatedContentService).also { eventBus.register(it) } + val igContextManager = IgContextManager(federatedContentService).also { eventBus.register(it) } + val compilationManager = CqlCompilationManager(federatedContentService, compilerOptionsManager, igContextManager) + + val languageClientFuture = CompletableFuture() + val commandsFuture = CompletableFuture>() + + val workspaceService = CqlWorkspaceService(languageClientFuture, commandsFuture, workspaceFolders, eventBus) + val textDocumentService = CqlTextDocumentService( + languageClientFuture, + HoverProvider(compilationManager), + FormattingProvider(federatedContentService), + eventBus + ) + + val contributions = mutableListOf() + ServiceLoader.load(CqlLanguageServerPluginFactory::class.java).forEach { factory -> + factory.createPlugin(languageClientFuture, workspaceService, textDocumentService, compilationManager) + .getCommandContribution()?.let { contributions.add(it) } + } + contributions.add(ViewElmCommandContribution(compilationManager)) + contributions.add(DebugCqlCommandContribution(igContextManager)) + commandsFuture.complete(contributions) + + val server = CqlLanguageServer(languageClientFuture, workspaceService, textDocumentService) + DiagnosticsService(languageClientFuture, compilationManager, federatedContentService).also { eventBus.register(it) } + + @Suppress("java:S106") + val launcher = LSPLauncher.createServerLauncher(server, System.`in`, System.out) + val client = launcher.remoteProxy + + server.connect(client) + languageClientFuture.complete(client) + val serverThread = launcher.startListening() + + log.info("java.version: {}", System.getProperty("java.version")) + log.info("cql-language-server version: {}", CqlLanguageServer::class.java.`package`.implementationVersion) + log.info("cql-translator version: {}", CqlTranslator::class.java.`package`.implementationVersion) + log.info("cql-engine version: {}", CqlEngine::class.java.`package`.implementationVersion) + log.info("cql-language-server started") + + val executor = Executors.newSingleThreadExecutor() + val connectionClosed = CompletableFuture.runAsync( + { + try { + serverThread.get() + log.info("LSP connection closed") + } catch (e: Exception) { + log.debug("Server thread exception", e) + } + }, + executor + ) + + try { + CompletableFuture.anyOf(server.exited(), connectionClosed).get() + } catch (e: Exception) { + log.error("Error waiting for shutdown", e) + } + + log.info("Shutting down language server") + serverThread.cancel(true) + executor.shutdownNow() + + System.exit(0) +} + +private fun configureLogging() { + SLF4JBridgeHandler.removeHandlersForRootLogger() + SLF4JBridgeHandler.install() +} diff --git a/ls/service/src/main/resources/application.properties b/ls/service/src/main/resources/application.properties deleted file mode 100644 index 6ef1a144..00000000 --- a/ls/service/src/main/resources/application.properties +++ /dev/null @@ -1,4 +0,0 @@ -spring.main.banner-mode=off -spring.main.log-startup-info=false -spring.profiles.active=default -spring.main.web-application-type=none \ No newline at end of file diff --git a/plugin/debug/pom.xml b/plugin/debug/pom.xml index 27283350..d34dabbd 100644 --- a/plugin/debug/pom.xml +++ b/plugin/debug/pom.xml @@ -38,4 +38,30 @@ provided + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + kapt + kapt + + + ${project.basedir}/src/main/java + + + + com.google.auto.service + auto-service + ${auto-service.version} + + + + + + + + \ No newline at end of file diff --git a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContribution.java b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContribution.java deleted file mode 100644 index a6c128a2..00000000 --- a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContribution.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.opencds.cqf.cql.ls.plugin.debug; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.ExecuteCommandParams; -import org.opencds.cqf.cql.ls.plugin.debug.session.DebugSession; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.plugin.CommandContribution; -import org.opencds.cqf.cql.ls.server.utility.Futures; - -public class DebugCommandContribution implements CommandContribution { - - public static final String START_DEBUG_COMMAND = "org.opencds.cqf.cql.ls.plugin.debug.startDebugSession"; - - private DebugSession debugSession = null; - - private CqlCompilationManager cqlCompilationManager; - - public DebugCommandContribution(CqlCompilationManager cqlCompilationManager) { - this.cqlCompilationManager = cqlCompilationManager; - } - - @Override - public Set getCommands() { - return Collections.singleton(START_DEBUG_COMMAND); - } - - @Override - public CompletableFuture executeCommand(ExecuteCommandParams params) { - if (START_DEBUG_COMMAND.equals(params.getCommand())) { - if (this.debugSession == null || !this.debugSession.isActive()) { - this.debugSession = new DebugSession(); - try { - return CompletableFuture.completedFuture( - this.debugSession.start().get()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return Futures.failed(e); - } catch (Exception e) { - return Futures.failed(e); - } - } else { - throw new IllegalStateException( - "Please wait for the current debug session to end before starting a new one."); - } - } else { - throw new IllegalArgumentException( - String.format("DebugPlugin doesn't support command %s", params.getCommand())); - } - } -} diff --git a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContribution.kt b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContribution.kt new file mode 100644 index 00000000..eeb3a9e8 --- /dev/null +++ b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContribution.kt @@ -0,0 +1,49 @@ +package org.opencds.cqf.cql.ls.plugin.debug + +import org.eclipse.lsp4j.ExecuteCommandParams +import org.opencds.cqf.cql.ls.plugin.debug.session.DebugSession +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.plugin.CommandContribution +import org.opencds.cqf.cql.ls.server.utility.Futures +import java.util.concurrent.CompletableFuture + +class DebugCommandContribution( + private val cqlCompilationManager: CqlCompilationManager +) : CommandContribution { + + companion object { + const val START_DEBUG_COMMAND = "org.opencds.cqf.cql.ls.plugin.debug.startDebugSession" + } + + private var debugSession: DebugSession? = null + + override fun getCommands(): Set { + return setOf(START_DEBUG_COMMAND) + } + + override fun executeCommand(params: ExecuteCommandParams): CompletableFuture { + if (START_DEBUG_COMMAND == params.command) { + val session = debugSession + if (session == null || !session.isActive()) { + val newSession = DebugSession() + debugSession = newSession + return try { + CompletableFuture.completedFuture(newSession.start().get()) + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + Futures.failed(e) + } catch (e: Exception) { + Futures.failed(e) + } + } else { + throw IllegalStateException( + "Please wait for the current debug session to end before starting a new one." + ) + } + } else { + throw IllegalArgumentException( + "DebugPlugin doesn't support command ${params.command}" + ) + } + } +} diff --git a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPlugin.java b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPlugin.java deleted file mode 100644 index de43429c..00000000 --- a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPlugin.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.opencds.cqf.cql.ls.plugin.debug; - -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.eclipse.lsp4j.services.WorkspaceService; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.plugin.CommandContribution; -import org.opencds.cqf.cql.ls.server.plugin.CqlLanguageServerPlugin; - -@SuppressWarnings("unused") // TODO: Remove once this class is completed -public class DebugPlugin implements CqlLanguageServerPlugin { - - @Override - public String getName() { - return "org.opencds.cqf.cql.ls.plugin.debug.DebugPlugin"; - } - - private CompletableFuture client; - private WorkspaceService workspaceService; - private TextDocumentService textDocumentService; - - private CqlCompilationManager compilationManager; - private CommandContribution commandContribution; - - public DebugPlugin( - CompletableFuture client, - WorkspaceService workspaceService, - TextDocumentService textDocumentService, - CqlCompilationManager compilationManager) { - this.client = client; - this.workspaceService = workspaceService; - this.textDocumentService = textDocumentService; - this.compilationManager = compilationManager; - - this.commandContribution = new DebugCommandContribution(this.compilationManager); - } - - @Override - public CommandContribution getCommandContribution() { - return this.commandContribution; - } -} diff --git a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPlugin.kt b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPlugin.kt new file mode 100644 index 00000000..18ec962c --- /dev/null +++ b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPlugin.kt @@ -0,0 +1,28 @@ +package org.opencds.cqf.cql.ls.plugin.debug + +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.services.TextDocumentService +import org.eclipse.lsp4j.services.WorkspaceService +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.plugin.CommandContribution +import org.opencds.cqf.cql.ls.server.plugin.CqlLanguageServerPlugin +import java.util.concurrent.CompletableFuture + +@Suppress("unused") // TODO: Remove once this class is completed +class DebugPlugin( + private val client: CompletableFuture, + private val workspaceService: WorkspaceService, + private val textDocumentService: TextDocumentService, + private val compilationManager: CqlCompilationManager +) : CqlLanguageServerPlugin { + + override fun getName(): String { + return "org.opencds.cqf.cql.ls.plugin.debug.DebugPlugin" + } + + private val commandContribution: CommandContribution = DebugCommandContribution(this.compilationManager) + + override fun getCommandContribution(): CommandContribution { + return this.commandContribution + } +} diff --git a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPluginFactory.java b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPluginFactory.java deleted file mode 100644 index 2c8d24f6..00000000 --- a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPluginFactory.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.opencds.cqf.cql.ls.plugin.debug; - -import com.google.auto.service.AutoService; -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.eclipse.lsp4j.services.WorkspaceService; -import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager; -import org.opencds.cqf.cql.ls.server.plugin.CqlLanguageServerPlugin; -import org.opencds.cqf.cql.ls.server.plugin.CqlLanguageServerPluginFactory; - -@AutoService(CqlLanguageServerPluginFactory.class) -public class DebugPluginFactory implements CqlLanguageServerPluginFactory { - - @Override - public CqlLanguageServerPlugin createPlugin( - CompletableFuture client, - WorkspaceService workspaceService, - TextDocumentService textDocumentService, - CqlCompilationManager compilationManager) { - return new DebugPlugin(client, workspaceService, textDocumentService, compilationManager); - } -} diff --git a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPluginFactory.kt b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPluginFactory.kt new file mode 100644 index 00000000..f8d6fd5a --- /dev/null +++ b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/DebugPluginFactory.kt @@ -0,0 +1,23 @@ +package org.opencds.cqf.cql.ls.plugin.debug + +import com.google.auto.service.AutoService +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.services.TextDocumentService +import org.eclipse.lsp4j.services.WorkspaceService +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.plugin.CqlLanguageServerPlugin +import org.opencds.cqf.cql.ls.server.plugin.CqlLanguageServerPluginFactory +import java.util.concurrent.CompletableFuture + +@AutoService(CqlLanguageServerPluginFactory::class) +class DebugPluginFactory : CqlLanguageServerPluginFactory { + + override fun createPlugin( + client: CompletableFuture, + workspaceService: WorkspaceService, + textDocumentService: TextDocumentService, + compilationManager: CqlCompilationManager + ): CqlLanguageServerPlugin { + return DebugPlugin(client, workspaceService, textDocumentService, compilationManager) + } +} diff --git a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSession.java b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSession.java deleted file mode 100644 index 209e30f8..00000000 --- a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSession.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.opencds.cqf.cql.ls.plugin.debug.session; - -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import org.eclipse.lsp4j.debug.launch.DSPLauncher; -import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; -import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.opencds.cqf.cql.debug.CqlDebugServer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DebugSession { - - private static Logger log = LoggerFactory.getLogger(DebugSession.class); - - private static ExecutorService threadService = Executors.newCachedThreadPool(); - - private CqlDebugServer debugServer; - - private Boolean isActive = false; - - private final CompletableFuture port = new CompletableFuture<>(); - - public DebugSession() { - this.debugServer = new CqlDebugServer(); - } - - public CompletableFuture start() { - synchronized (this) { - this.isActive = true; - } - startListening(); - return this.port; - } - - public CqlDebugServer getDebugServer() { - return this.debugServer; - } - - public Boolean isActive() { - return this.isActive; - } - - private void startListening() { - threadService.submit(() -> { - try (ServerSocket serverSocket = new ServerSocket(0)) { - serverSocket.setSoTimeout(10000); - this.port.complete(serverSocket.getLocalPort()); - Socket s = serverSocket.accept(); - Launcher launcher = DSPLauncher.createServerLauncher( - this.getDebugServer(), s.getInputStream(), s.getOutputStream()); - this.getDebugServer().connect(launcher.getRemoteProxy()); - - // We'll exit the server when the client disconnects. - Future serverThread = launcher.startListening(); - this.getDebugServer().exited().get(); - serverThread.cancel(true); - } catch (IOException e) { - log.error("failed to launch debug server for debug session", e); - this.port.completeExceptionally(e); - } catch (CancellationException e) { - log.debug("debug session cancelled", e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.debug("debug session interrupted"); - } catch (Exception e) { - log.error("error in debug session", e); - } - synchronized (this) { - this.isActive = false; - } - }); - } -} diff --git a/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSession.kt b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSession.kt new file mode 100644 index 00000000..f3b6d1ec --- /dev/null +++ b/plugin/debug/src/main/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSession.kt @@ -0,0 +1,77 @@ +package org.opencds.cqf.cql.ls.plugin.debug.session + +import org.eclipse.lsp4j.debug.launch.DSPLauncher +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient +import org.opencds.cqf.cql.debug.CqlDebugServer +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.ServerSocket +import java.util.concurrent.CancellationException +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class DebugSession { + + companion object { + private val log = LoggerFactory.getLogger(DebugSession::class.java) + private val threadService: ExecutorService = Executors.newCachedThreadPool() + } + + private val debugServer: CqlDebugServer = CqlDebugServer() + + @Volatile + private var isActiveFlag: Boolean = false + + private val port: CompletableFuture = CompletableFuture() + + fun start(): CompletableFuture { + synchronized(this) { + isActiveFlag = true + } + startListening() + return this.port + } + + fun getDebugServer(): CqlDebugServer { + return this.debugServer + } + + fun isActive(): Boolean { + return this.isActiveFlag + } + + private fun startListening() { + threadService.submit { + try { + ServerSocket(0).use { serverSocket -> + serverSocket.soTimeout = 10000 + this.port.complete(serverSocket.localPort) + val s = serverSocket.accept() + val launcher = DSPLauncher.createServerLauncher( + this.getDebugServer(), s.getInputStream(), s.getOutputStream() + ) + this.getDebugServer().connect(launcher.remoteProxy) + + // We'll exit the server when the client disconnects. + val serverThread = launcher.startListening() + this.getDebugServer().exited().get() + serverThread.cancel(true) + } + } catch (e: IOException) { + log.error("failed to launch debug server for debug session", e) + this.port.completeExceptionally(e) + } catch (e: CancellationException) { + log.debug("debug session cancelled", e) + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + log.debug("debug session interrupted") + } catch (e: Exception) { + log.error("error in debug session", e) + } + synchronized(this) { + isActiveFlag = false + } + } + } +} diff --git a/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/client/TestDebugClient.java b/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/client/TestDebugClient.java deleted file mode 100644 index d53b41dd..00000000 --- a/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/client/TestDebugClient.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.opencds.cqf.cql.ls.plugin.debug.client; - -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.debug.ExitedEventArguments; -import org.eclipse.lsp4j.debug.OutputEventArguments; -import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; - -public class TestDebugClient implements IDebugProtocolClient { - private String serverOutput = null; - - private CompletableFuture exited; - - public TestDebugClient() { - this.exited = new CompletableFuture<>(); - } - - public String getServerOutput() { - return this.serverOutput; - } - - @Override - public void initialized() {} - - @Override - public void output(OutputEventArguments args) { - // this.serverOutput = args.getOutput(); - - } - - @Override - public void exited(ExitedEventArguments args) { - this.serverOutput = "got exited"; - this.exited.complete(null); - } - - public CompletableFuture exited() { - return this.exited; - } -} diff --git a/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/client/TestDebugClient.kt b/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/client/TestDebugClient.kt new file mode 100644 index 00000000..3f680434 --- /dev/null +++ b/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/client/TestDebugClient.kt @@ -0,0 +1,31 @@ +package org.opencds.cqf.cql.ls.plugin.debug.client + +import org.eclipse.lsp4j.debug.ExitedEventArguments +import org.eclipse.lsp4j.debug.OutputEventArguments +import org.eclipse.lsp4j.debug.services.IDebugProtocolClient +import java.util.concurrent.CompletableFuture + +class TestDebugClient : IDebugProtocolClient { + private var serverOutput: String? = null + + private val exitedFuture: CompletableFuture = CompletableFuture() + + fun getServerOutput(): String? { + return this.serverOutput + } + + override fun initialized() {} + + override fun output(args: OutputEventArguments) { + // this.serverOutput = args.output + } + + override fun exited(args: ExitedEventArguments) { + this.serverOutput = "got exited" + this.exitedFuture.complete(null) + } + + fun exited(): CompletableFuture { + return this.exitedFuture + } +} diff --git a/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSessionTest.java b/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSessionTest.java deleted file mode 100644 index fca52fb0..00000000 --- a/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSessionTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.opencds.cqf.cql.ls.plugin.debug.session; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.net.Socket; -import java.util.concurrent.Future; -import org.eclipse.lsp4j.debug.ConfigurationDoneArguments; -import org.eclipse.lsp4j.debug.DisconnectArguments; -import org.eclipse.lsp4j.debug.InitializeRequestArguments; -import org.eclipse.lsp4j.debug.launch.DSPLauncher; -import org.eclipse.lsp4j.debug.services.IDebugProtocolServer; -import org.eclipse.lsp4j.jsonrpc.Launcher; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.cql.ls.plugin.debug.client.TestDebugClient; - -class DebugSessionTest { - - // This test starts a Debug session on a background thread - // which listens at a random socket. It creates a dummy client to - // connect to that socket. - @Test - void simpleSessionTest() throws Exception { - - DebugSession session = new DebugSession(); - - // This starts a debug session on another thread - Integer port = session.start().join(); - TestDebugClient client = new TestDebugClient(); - - try (Socket socket = new Socket("localhost", port)) { - - Launcher launcher = - DSPLauncher.createClientLauncher(client, socket.getInputStream(), socket.getOutputStream()); - Future clientThread = launcher.startListening(); - IDebugProtocolServer server = launcher.getRemoteProxy(); - server.initialize(new InitializeRequestArguments()).get(); - server.configurationDone(new ConfigurationDoneArguments()).get(); - server.disconnect(new DisconnectArguments()).get(); - client.exited().get(); - clientThread.cancel(true); - } catch (Exception e) { - throw new RuntimeException("error starting client", e); - } - - assertNotNull(client.getServerOutput()); - assertEquals("got exited", client.getServerOutput()); - } -} diff --git a/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSessionTest.kt b/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSessionTest.kt new file mode 100644 index 00000000..98004d6d --- /dev/null +++ b/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/session/DebugSessionTest.kt @@ -0,0 +1,47 @@ +package org.opencds.cqf.cql.ls.plugin.debug.session + +import org.eclipse.lsp4j.debug.ConfigurationDoneArguments +import org.eclipse.lsp4j.debug.DisconnectArguments +import org.eclipse.lsp4j.debug.InitializeRequestArguments +import org.eclipse.lsp4j.debug.launch.DSPLauncher +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.plugin.debug.client.TestDebugClient +import java.net.Socket + +class DebugSessionTest { + + // This test starts a Debug session on a background thread + // which listens at a random socket. It creates a dummy client to + // connect to that socket. + @Test + fun simpleSessionTest() { + val session = DebugSession() + + // This starts a debug session on another thread + val port: Int = session.start().join() + val client = TestDebugClient() + + try { + Socket("localhost", port).use { socket -> + val launcher = DSPLauncher.createClientLauncher( + client, socket.getInputStream(), socket.getOutputStream() + ) + val clientThread = launcher.startListening() + val server: IDebugProtocolServer = launcher.remoteProxy + server.initialize(InitializeRequestArguments()).get() + server.configurationDone(ConfigurationDoneArguments()).get() + server.disconnect(DisconnectArguments()).get() + client.exited().get() + clientThread.cancel(true) + } + } catch (e: Exception) { + throw RuntimeException("error starting client", e) + } + + assertNotNull(client.getServerOutput()) + assertEquals("got exited", client.getServerOutput()) + } +} diff --git a/pom.xml b/pom.xml index c7385e5b..dbabedd3 100644 --- a/pom.xml +++ b/pom.xml @@ -299,13 +299,26 @@ com.diffplug.spotless spotless-maven-plugin - 2.39.0 + 2.44.0 2.38.0 + + + 1.2.1 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + 17 @@ -421,9 +434,54 @@ org.apache.maven.plugins maven-checkstyle-plugin + + org.jetbrains.kotlin + kotlin-maven-plugin + + + compile + compile + + + ${project.basedir}/src/main/java + + + + + test-compile + test-compile + + + ${project.basedir}/src/test/java + ${project.basedir}/src/test/kotlin + + + + + org.apache.maven.plugins maven-compiler-plugin + + + default-compile + none + + + default-testCompile + none + + + java-compile + compile + compile + + + java-test-compile + test-compile + testCompile + + org.apache.maven.plugins From 06532ab6b5cfb591a98f697642ceda7655b586d0 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 16 Mar 2026 09:29:16 -0400 Subject: [PATCH 02/19] fixes test cases that fail in github --- .../service/FederatedContentServiceTest.kt | 38 +++++++++++++------ pom.xml | 15 +++++++- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt index ade42beb..c4f385ab 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt @@ -1,13 +1,17 @@ package org.opencds.cqf.cql.ls.server.service +import org.eclipse.lsp4j.DidOpenTextDocumentParams +import org.eclipse.lsp4j.TextDocumentItem import org.hl7.elm.r1.VersionedIdentifier -import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.Mockito.mock import org.mockito.Mockito.`when` import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent import java.io.ByteArrayInputStream import java.net.URI @@ -18,19 +22,33 @@ class FederatedContentServiceTest { @BeforeEach fun setUp() { - activeService = mock(ActiveContentService::class.java) + activeService = ActiveContentService() fileService = mock(ContentService::class.java) fedService = FederatedContentService(activeService, fileService) } + private fun openDoc( + uri: String, + content: String, + ) { + val params = DidOpenTextDocumentParams() + val item = TextDocumentItem() + item.uri = uri + item.text = content + item.version = 1 + params.textDocument = item + activeService.didOpen(DidOpenTextDocumentEvent(params)) + } + // ----------------------------------------------------------------------- // locate — merges results from both services // ----------------------------------------------------------------------- @Test fun locate_mergesActiveAndFileResults() { + openDoc(ACTIVE_URI.toString(), "library One version '1.0.0'\ndefine \"X\": 1") + val id = VersionedIdentifier().withId("One").withVersion("1.0.0") - `when`(activeService.locate(ROOT, id)).thenReturn(mutableSetOf(ACTIVE_URI)) `when`(fileService.locate(ROOT, id)).thenReturn(mutableSetOf(FILE_URI)) val result = fedService.locate(ROOT, id) @@ -42,7 +60,6 @@ class FederatedContentServiceTest { @Test fun locate_emptyFromBoth_returnsEmptySet() { val id = VersionedIdentifier().withId("Unknown") - `when`(activeService.locate(ROOT, id)).thenReturn(mutableSetOf()) `when`(fileService.locate(ROOT, id)).thenReturn(mutableSetOf()) val result = fedService.locate(ROOT, id) @@ -55,25 +72,24 @@ class FederatedContentServiceTest { // ----------------------------------------------------------------------- @Test - fun read_uriInActiveSet_returnsActiveStream() { - val expected = ByteArrayInputStream("active content".toByteArray()) - `when`(activeService.activeUris()).thenReturn(setOf(ACTIVE_URI)) - `when`(activeService.read(ACTIVE_URI)).thenReturn(expected) + fun read_uriInActiveSet_returnsActiveContent() { + openDoc(ACTIVE_URI.toString(), "library One version '1.0.0'\ndefine \"X\": 1") val result = fedService.read(ACTIVE_URI) - assertSame(expected, result) + assertNotNull(result) } @Test fun read_uriNotInActiveSet_fallsBackToFileService() { val expected = ByteArrayInputStream("file content".toByteArray()) - `when`(activeService.activeUris()).thenReturn(emptySet()) `when`(fileService.read(FILE_URI)).thenReturn(expected) + // FILE_URI was never opened in activeService, so it falls through to fileService val result = fedService.read(FILE_URI) - assertSame(expected, result) + assertNull(activeService.read(FILE_URI)) // confirm not in active set + assertNotNull(result) } companion object { diff --git a/pom.xml b/pom.xml index dbabedd3..51bf39d1 100644 --- a/pom.xml +++ b/pom.xml @@ -280,7 +280,20 @@ org.jacoco jacoco-maven-plugin - 0.8.11 + 0.8.12 + + + + **/*$DefaultImpls.class + + **/*$Companion.class + + **/*$WhenMappings.class + + From 6642c75fa3234f884e66f7b1991b2e5c7f17c897 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 16 Mar 2026 09:44:14 -0400 Subject: [PATCH 03/19] fixes windows related path issue --- .../service/FederatedContentServiceTest.kt | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt index c4f385ab..eb7f9d94 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/FederatedContentServiceTest.kt @@ -4,7 +4,6 @@ import org.eclipse.lsp4j.DidOpenTextDocumentParams import org.eclipse.lsp4j.TextDocumentItem import org.hl7.elm.r1.VersionedIdentifier import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -28,12 +27,12 @@ class FederatedContentServiceTest { } private fun openDoc( - uri: String, + uriString: String, content: String, ) { val params = DidOpenTextDocumentParams() val item = TextDocumentItem() - item.uri = uri + item.uri = uriString item.text = content item.version = 1 params.textDocument = item @@ -46,23 +45,28 @@ class FederatedContentServiceTest { @Test fun locate_mergesActiveAndFileResults() { - openDoc(ACTIVE_URI.toString(), "library One version '1.0.0'\ndefine \"X\": 1") + openDoc("file:///workspace/One.cql", "library One version '1.0.0'\ndefine \"X\": 1") + + // Retrieve the URI exactly as stored — Uris.parseOrNull normalizes differently per platform + val storedUri = activeService.activeUris().first() + val fileOnlyUri = URI.create("file:///other/lib/One.cql") val id = VersionedIdentifier().withId("One").withVersion("1.0.0") - `when`(fileService.locate(ROOT, id)).thenReturn(mutableSetOf(FILE_URI)) + `when`(fileService.locate(storedUri, id)).thenReturn(setOf(fileOnlyUri)) - val result = fedService.locate(ROOT, id) + val result = fedService.locate(storedUri, id) - assertTrue(result.contains(ACTIVE_URI)) - assertTrue(result.contains(FILE_URI)) + assertTrue(result.contains(storedUri)) + assertTrue(result.contains(fileOnlyUri)) } @Test fun locate_emptyFromBoth_returnsEmptySet() { + val root = URI.create("file:///workspace/") val id = VersionedIdentifier().withId("Unknown") - `when`(fileService.locate(ROOT, id)).thenReturn(mutableSetOf()) + `when`(fileService.locate(root, id)).thenReturn(emptySet()) - val result = fedService.locate(ROOT, id) + val result = fedService.locate(root, id) assertTrue(result.isEmpty()) } @@ -73,28 +77,24 @@ class FederatedContentServiceTest { @Test fun read_uriInActiveSet_returnsActiveContent() { - openDoc(ACTIVE_URI.toString(), "library One version '1.0.0'\ndefine \"X\": 1") + openDoc("file:///workspace/One.cql", "library One version '1.0.0'\ndefine \"X\": 1") - val result = fedService.read(ACTIVE_URI) + // Use the actual stored URI to avoid platform-specific normalization differences + val storedUri = activeService.activeUris().first() + val result = fedService.read(storedUri) assertNotNull(result) } @Test fun read_uriNotInActiveSet_fallsBackToFileService() { + val fileUri = URI.create("file:///workspace/lib/One.cql") val expected = ByteArrayInputStream("file content".toByteArray()) - `when`(fileService.read(FILE_URI)).thenReturn(expected) + `when`(fileService.read(fileUri)).thenReturn(expected) - // FILE_URI was never opened in activeService, so it falls through to fileService - val result = fedService.read(FILE_URI) + // fileUri was never opened in activeService, so fedService falls through to fileService + val result = fedService.read(fileUri) - assertNull(activeService.read(FILE_URI)) // confirm not in active set assertNotNull(result) } - - companion object { - private val ROOT: URI = URI.create("file:///workspace/") - private val ACTIVE_URI: URI = URI.create("file:///workspace/One.cql") - private val FILE_URI: URI = URI.create("file:///workspace/lib/One.cql") - } } From 5e483abd12774c7bdec203f2f23c3ce47ad91e35 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 16 Mar 2026 11:51:03 -0400 Subject: [PATCH 04/19] adds more test coverage, cleans up some tests --- .../cqf/cql/ls/server/command/CqlCommand.kt | 3 +- .../ig/standard/IgStandardRepository.kt | 2 +- .../server/service/CqlTextDocumentService.kt | 4 +- .../ls/server/service/DiagnosticsService.kt | 50 ++++--- .../manager/CqlCompilationManagerTest.kt | 126 +++++++++++++++++ .../ls/server/manager/IgContextManagerTest.kt | 128 ++++++++++++++++++ 6 files changed, 283 insertions(+), 30 deletions(-) create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManagerTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt index 7c1a1915..bfca53ae 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt @@ -128,6 +128,7 @@ class CqlCommand : Callable { log.debug("{}: {}", logCategory, s) } + @Suppress("OVERRIDE_DEPRECATION") override fun isDebugLogging(): Boolean = log.isDebugEnabled } @@ -152,7 +153,7 @@ class CqlCommand : Callable { if (rootDir != null && igPath != null) { igContext = IGContext(Logger()) igContext.initializeFromIg(rootDir, igPath, toVersionNumber(fhirVersionEnum)) - } else if (parentCommand != null && parentCommand!!.getIgContextManager() != null && rootDir != null) { + } else if (parentCommand != null && rootDir != null) { npmProcessor = parentCommand!! .getIgContextManager() .getContext(Uris.addPath(Uris.addPath(java.net.URI.create(rootDir), "input")!!, "cql")!!) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt index d603ab30..281c6b7d 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt @@ -430,7 +430,7 @@ open class IgStandardRepository : IRepository { val compartment = compartmentFrom(headers) val resourceIdMap = readDirectoryForResourceType(resourceType, compartment) - if (searchParameters == null || searchParameters.isEmpty) { + if (searchParameters.isEmpty) { resourceIdMap.values.forEach { builder.addCollectionEntry(it) } return builder.bundle as B } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt index 337853b2..5094ade0 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt @@ -56,8 +56,8 @@ class CqlTextDocumentService( .exceptionally { notifyClient(it) } as CompletableFuture } - override fun formatting(params: DocumentFormattingParams): CompletableFuture> { - return CompletableFuture.supplyAsync> { + override fun formatting(params: DocumentFormattingParams): CompletableFuture> { + return CompletableFuture.supplyAsync> { formattingProvider.format(params.textDocument.uri) }.exceptionally { notifyClient(it) } } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt index 91f6de9b..211d8f97 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt @@ -115,32 +115,30 @@ class DiagnosticsService( libraryUris[VersionedIdentifier().withId("unknown")] = uri for (exception in exceptions) { - if (exception != null) { - val exLocator = exception.locator - val exLibrary = exLocator?.library - var eUri = if (exLibrary != null && exLibrary.id != null) { - libraryUris[exLibrary] - } else null - - if (eUri == null) { - eUri = uri // put all unknown or indeterminate errors to the current uri so at least they get reported - } - - val d = Diagnostics.convert(exception) - - if (d != null) { - log.debug( - "diagnostic: {} {}:{}-{}:{}: {}", - eUri, - d.range.start.line, - d.range.start.character, - d.range.end.line, - d.range.end.character, - d.message - ) - - diagnostics.getOrPut(eUri) { mutableSetOf() }.add(d) - } + val exLocator = exception.locator + val exLibrary = exLocator?.library + var eUri = if (exLibrary != null && exLibrary.id != null) { + libraryUris[exLibrary] + } else null + + if (eUri == null) { + eUri = uri // put all unknown or indeterminate errors to the current uri so at least they get reported + } + + val d = Diagnostics.convert(exception) + + if (d != null) { + log.debug( + "diagnostic: {} {}:{}-{}:{}: {}", + eUri, + d.range.start.line, + d.range.start.character, + d.range.end.line, + d.range.end.character, + d.message + ) + + diagnostics.getOrPut(eUri) { mutableSetOf() }.add(d) } } diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManagerTest.kt new file mode 100644 index 00000000..5f1024ee --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManagerTest.kt @@ -0,0 +1,126 @@ +package org.opencds.cqf.cql.ls.server.manager + +import org.cqframework.cql.cql2elm.CqlCompilerException +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.service.TestContentService +import java.io.InputStream +import java.net.URI + +class CqlCompilationManagerTest { + + companion object { + private val cs = TestContentService() + private lateinit var manager: CqlCompilationManager + + private val ONE_URI: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/One.cql")!! + private val TWO_URI: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/Two.cql")!! + private val SYNTAX_ERROR_URI: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/SyntaxError.cql")!! + private val MISSING_INCLUDE_URI: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/MissingInclude.cql")!! + + @BeforeAll + @JvmStatic + fun beforeAll() { + manager = CqlCompilationManager(cs, CompilerOptionsManager(cs), IgContextManager(cs)) + } + } + + // ----------------------------------------------------------------------- + // compile(URI) — null when content service has no content for the URI + // ----------------------------------------------------------------------- + + @Test + fun compile_uri_returnsNull_whenContentServiceReturnsNull() { + val nullCs = object : ContentService { + override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = emptySet() + override fun read(uri: URI): InputStream? = null + } + val localManager = CqlCompilationManager(nullCs, CompilerOptionsManager(nullCs), IgContextManager(nullCs)) + val result = localManager.compile(URI("file:///nonexistent.cql")) + assertNull(result) + } + + // ----------------------------------------------------------------------- + // compile(URI) — valid CQL produces a compiler with no errors + // ----------------------------------------------------------------------- + + @Test + fun compile_validCql_returnsNonNull() { + val compiler = manager.compile(ONE_URI) + assertNotNull(compiler) + } + + @Test + fun compile_validCql_hasNoErrors() { + val compiler = manager.compile(ONE_URI)!! + val errors = compiler.exceptions.filter { + it.severity == CqlCompilerException.ErrorSeverity.Error + } + assertTrue(errors.isEmpty(), "Expected no errors for One.cql") + } + + @Test + fun compile_validCqlWithInclude_hasNoErrors() { + val compiler = manager.compile(TWO_URI)!! + val errors = compiler.exceptions.filter { + it.severity == CqlCompilerException.ErrorSeverity.Error + } + assertTrue(errors.isEmpty(), "Expected no errors for Two.cql (which includes One.cql)") + } + + // ----------------------------------------------------------------------- + // compile(URI) — invalid CQL produces errors + // ----------------------------------------------------------------------- + + @Test + fun compile_syntaxError_returnsNonNull() { + val compiler = manager.compile(SYNTAX_ERROR_URI) + assertNotNull(compiler) + } + + @Test + fun compile_syntaxError_hasErrors() { + val compiler = manager.compile(SYNTAX_ERROR_URI)!! + val errors = compiler.exceptions.filter { + it.severity == CqlCompilerException.ErrorSeverity.Error + } + assertFalse(errors.isEmpty(), "Expected errors for CQL with syntax errors") + } + + @Test + fun compile_missingInclude_hasErrors() { + val compiler = manager.compile(MISSING_INCLUDE_URI)!! + assertFalse( + compiler.exceptions.isEmpty(), + "Expected exceptions for CQL with a missing include" + ) + } + + // ----------------------------------------------------------------------- + // compile(URI, InputStream) — accepts a stream directly (bypasses read()) + // ----------------------------------------------------------------------- + + @Test + fun compile_stream_returnsNonNull() { + val stream = cs.read(ONE_URI)!! + val compiler = manager.compile(ONE_URI, stream) + assertNotNull(compiler) + } + + @Test + fun compile_stream_validCql_hasNoErrors() { + val stream = cs.read(ONE_URI)!! + val compiler = manager.compile(ONE_URI, stream) + val errors = compiler.exceptions.filter { + it.severity == CqlCompilerException.ErrorSeverity.Error + } + assertTrue(errors.isEmpty(), "Expected no errors when compiling One.cql from stream") + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt new file mode 100644 index 00000000..6aa943ed --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt @@ -0,0 +1,128 @@ +package org.opencds.cqf.cql.ls.server.manager + +import org.eclipse.lsp4j.DidChangeWatchedFilesParams +import org.eclipse.lsp4j.FileChangeType +import org.eclipse.lsp4j.FileEvent +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent +import org.opencds.cqf.cql.ls.server.service.TestContentService +import java.io.InputStream +import java.net.URI + +class IgContextManagerTest { + + private lateinit var manager: IgContextManager + + @BeforeEach + fun setUp() { + manager = IgContextManager(TestContentService()) + } + + // ----------------------------------------------------------------------- + // getContext — no ig.ini in fixture classpath paths → returns null + // ----------------------------------------------------------------------- + + @Test + fun getContext_noIgIni_returnsNull() { + val result = manager.getContext(TEST_URI) + assertNull(result) + } + + // ----------------------------------------------------------------------- + // getContext — result is cached (content service not re-read on second call) + // ----------------------------------------------------------------------- + + @Test + fun getContext_secondCall_returnsSameNullResult() { + val first = manager.getContext(TEST_URI) + val second = manager.getContext(TEST_URI) + assertEquals(first, second) + } + + @Test + fun getContext_cachedResult_doesNotReReadContentService() { + var readCount = 0 + val countingCs = object : ContentService { + override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = emptySet() + override fun read(uri: URI): InputStream? { + readCount++ + return null + } + } + val localManager = IgContextManager(countingCs) + + localManager.getContext(TEST_URI) // populates cache + val countAfterFirst = readCount + localManager.getContext(TEST_URI) // should hit cache + + assertEquals(countAfterFirst, readCount, "Second getContext() call should not read from content service again") + } + + // ----------------------------------------------------------------------- + // onMessageEvent — ig.ini change clears cache so next call re-reads + // ----------------------------------------------------------------------- + + @Test + fun onMessageEvent_igIniChanged_clearsCache() { + var readCount = 0 + val countingCs = object : ContentService { + override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = emptySet() + override fun read(uri: URI): InputStream? { + readCount++ + return null + } + } + val localManager = IgContextManager(countingCs) + + localManager.getContext(TEST_URI) // populates cache + val countAfterFirst = readCount + + val igIniUri = Uris.getHead(TEST_URI).toString() + "/ig.ini" + localManager.onMessageEvent( + DidChangeWatchedFilesEvent( + DidChangeWatchedFilesParams(listOf(FileEvent(igIniUri, FileChangeType.Changed))) + ) + ) + + localManager.getContext(TEST_URI) // cache cleared → re-reads + assertTrue(readCount > countAfterFirst, "Expected content service to be re-read after ig.ini change") + } + + @Test + fun onMessageEvent_unrelatedFile_doesNotClearCache() { + var readCount = 0 + val countingCs = object : ContentService { + override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = emptySet() + override fun read(uri: URI): InputStream? { + readCount++ + return null + } + } + val localManager = IgContextManager(countingCs) + + localManager.getContext(TEST_URI) // populates cache + val countAfterFirst = readCount + + localManager.onMessageEvent( + DidChangeWatchedFilesEvent( + DidChangeWatchedFilesParams(listOf(FileEvent("file:///workspace/SomeOther.json", FileChangeType.Changed))) + ) + ) + + localManager.getContext(TEST_URI) // should still be cached + assertEquals(countAfterFirst, readCount, "Unrelated file change should not clear cache") + } + + companion object { + // A URI whose parent dir is /org/opencds/cqf/cql/ls/server/ — + // TestContentService returns null for the ig.ini probe, so NpmProcessor is never created. + private val TEST_URI: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/One.cql")!! + } +} From 8427159f25684ee4e82f90254c855dcecb539a7e Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 16 Mar 2026 11:53:56 -0400 Subject: [PATCH 05/19] spotless:apply --- .../manager/CqlCompilationManagerTest.kt | 44 ++++++++------ .../ls/server/manager/IgContextManagerTest.kt | 60 ++++++++++++------- 2 files changed, 63 insertions(+), 41 deletions(-) diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManagerTest.kt index 5f1024ee..c9998655 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManagerTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CqlCompilationManagerTest.kt @@ -15,7 +15,6 @@ import java.io.InputStream import java.net.URI class CqlCompilationManagerTest { - companion object { private val cs = TestContentService() private lateinit var manager: CqlCompilationManager @@ -38,10 +37,15 @@ class CqlCompilationManagerTest { @Test fun compile_uri_returnsNull_whenContentServiceReturnsNull() { - val nullCs = object : ContentService { - override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = emptySet() - override fun read(uri: URI): InputStream? = null - } + val nullCs = + object : ContentService { + override fun locate( + root: URI, + libraryIdentifier: VersionedIdentifier, + ): Set = emptySet() + + override fun read(uri: URI): InputStream? = null + } val localManager = CqlCompilationManager(nullCs, CompilerOptionsManager(nullCs), IgContextManager(nullCs)) val result = localManager.compile(URI("file:///nonexistent.cql")) assertNull(result) @@ -60,18 +64,20 @@ class CqlCompilationManagerTest { @Test fun compile_validCql_hasNoErrors() { val compiler = manager.compile(ONE_URI)!! - val errors = compiler.exceptions.filter { - it.severity == CqlCompilerException.ErrorSeverity.Error - } + val errors = + compiler.exceptions.filter { + it.severity == CqlCompilerException.ErrorSeverity.Error + } assertTrue(errors.isEmpty(), "Expected no errors for One.cql") } @Test fun compile_validCqlWithInclude_hasNoErrors() { val compiler = manager.compile(TWO_URI)!! - val errors = compiler.exceptions.filter { - it.severity == CqlCompilerException.ErrorSeverity.Error - } + val errors = + compiler.exceptions.filter { + it.severity == CqlCompilerException.ErrorSeverity.Error + } assertTrue(errors.isEmpty(), "Expected no errors for Two.cql (which includes One.cql)") } @@ -88,9 +94,10 @@ class CqlCompilationManagerTest { @Test fun compile_syntaxError_hasErrors() { val compiler = manager.compile(SYNTAX_ERROR_URI)!! - val errors = compiler.exceptions.filter { - it.severity == CqlCompilerException.ErrorSeverity.Error - } + val errors = + compiler.exceptions.filter { + it.severity == CqlCompilerException.ErrorSeverity.Error + } assertFalse(errors.isEmpty(), "Expected errors for CQL with syntax errors") } @@ -99,7 +106,7 @@ class CqlCompilationManagerTest { val compiler = manager.compile(MISSING_INCLUDE_URI)!! assertFalse( compiler.exceptions.isEmpty(), - "Expected exceptions for CQL with a missing include" + "Expected exceptions for CQL with a missing include", ) } @@ -118,9 +125,10 @@ class CqlCompilationManagerTest { fun compile_stream_validCql_hasNoErrors() { val stream = cs.read(ONE_URI)!! val compiler = manager.compile(ONE_URI, stream) - val errors = compiler.exceptions.filter { - it.severity == CqlCompilerException.ErrorSeverity.Error - } + val errors = + compiler.exceptions.filter { + it.severity == CqlCompilerException.ErrorSeverity.Error + } assertTrue(errors.isEmpty(), "Expected no errors when compiling One.cql from stream") } } diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt index 6aa943ed..bcd4a677 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt @@ -17,7 +17,6 @@ import java.io.InputStream import java.net.URI class IgContextManagerTest { - private lateinit var manager: IgContextManager @BeforeEach @@ -49,13 +48,18 @@ class IgContextManagerTest { @Test fun getContext_cachedResult_doesNotReReadContentService() { var readCount = 0 - val countingCs = object : ContentService { - override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = emptySet() - override fun read(uri: URI): InputStream? { - readCount++ - return null + val countingCs = + object : ContentService { + override fun locate( + root: URI, + libraryIdentifier: VersionedIdentifier, + ): Set = emptySet() + + override fun read(uri: URI): InputStream? { + readCount++ + return null + } } - } val localManager = IgContextManager(countingCs) localManager.getContext(TEST_URI) // populates cache @@ -72,13 +76,18 @@ class IgContextManagerTest { @Test fun onMessageEvent_igIniChanged_clearsCache() { var readCount = 0 - val countingCs = object : ContentService { - override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = emptySet() - override fun read(uri: URI): InputStream? { - readCount++ - return null + val countingCs = + object : ContentService { + override fun locate( + root: URI, + libraryIdentifier: VersionedIdentifier, + ): Set = emptySet() + + override fun read(uri: URI): InputStream? { + readCount++ + return null + } } - } val localManager = IgContextManager(countingCs) localManager.getContext(TEST_URI) // populates cache @@ -87,8 +96,8 @@ class IgContextManagerTest { val igIniUri = Uris.getHead(TEST_URI).toString() + "/ig.ini" localManager.onMessageEvent( DidChangeWatchedFilesEvent( - DidChangeWatchedFilesParams(listOf(FileEvent(igIniUri, FileChangeType.Changed))) - ) + DidChangeWatchedFilesParams(listOf(FileEvent(igIniUri, FileChangeType.Changed))), + ), ) localManager.getContext(TEST_URI) // cache cleared → re-reads @@ -98,13 +107,18 @@ class IgContextManagerTest { @Test fun onMessageEvent_unrelatedFile_doesNotClearCache() { var readCount = 0 - val countingCs = object : ContentService { - override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = emptySet() - override fun read(uri: URI): InputStream? { - readCount++ - return null + val countingCs = + object : ContentService { + override fun locate( + root: URI, + libraryIdentifier: VersionedIdentifier, + ): Set = emptySet() + + override fun read(uri: URI): InputStream? { + readCount++ + return null + } } - } val localManager = IgContextManager(countingCs) localManager.getContext(TEST_URI) // populates cache @@ -112,8 +126,8 @@ class IgContextManagerTest { localManager.onMessageEvent( DidChangeWatchedFilesEvent( - DidChangeWatchedFilesParams(listOf(FileEvent("file:///workspace/SomeOther.json", FileChangeType.Changed))) - ) + DidChangeWatchedFilesParams(listOf(FileEvent("file:///workspace/SomeOther.json", FileChangeType.Changed))), + ), ) localManager.getContext(TEST_URI) // should still be cached From 0ed79ba9b056a40255b495f75dc37cc06b3a6f56 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 16 Mar 2026 12:12:31 -0400 Subject: [PATCH 06/19] adds more tests --- .../ContentServiceModelInfoProviderTest.kt | 31 +++++++++++++++++++ .../cqf/cql/ls/server/utility/FuturesTest.kt | 23 ++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProviderTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/utility/FuturesTest.kt diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProviderTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProviderTest.kt new file mode 100644 index 00000000..9e4f2c2a --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/provider/ContentServiceModelInfoProviderTest.kt @@ -0,0 +1,31 @@ +package org.opencds.cqf.cql.ls.server.provider + +import org.hl7.cql.model.ModelIdentifier +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertNull +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.InputStream +import java.net.URI + +class ContentServiceModelInfoProviderTest { + private val root = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server")!! + + @Test + fun load_returnsNull_whenContentServiceReturnsNull() { + val provider = + ContentServiceModelInfoProvider( + root, + object : ContentService { + override fun locate( + root: URI, + identifier: VersionedIdentifier, + ): Set = emptySet() + + override fun read(uri: URI): InputStream? = null + }, + ) + assertNull(provider.load(ModelIdentifier(id = "NonExistentModel"))) + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/utility/FuturesTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/utility/FuturesTest.kt new file mode 100644 index 00000000..3f2a33d6 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/utility/FuturesTest.kt @@ -0,0 +1,23 @@ +package org.opencds.cqf.cql.ls.server.utility + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.concurrent.ExecutionException + +class FuturesTest { + @Test + fun failed_returnsCompletedExceptionally() { + val future = Futures.failed(RuntimeException("test")) + assertTrue(future.isCompletedExceptionally) + } + + @Test + fun failed_wrapsExceptionAsCause() { + val ex = RuntimeException("test error") + val future = Futures.failed(ex) + val thrown = assertThrows { future.get() } + assertEquals(ex, thrown.cause) + } +} From 7cac6884b5059d8125e43c5a569a797a9376fbaf Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Fri, 20 Mar 2026 22:10:37 -0400 Subject: [PATCH 07/19] addresses issue with cross-platform file/folder issues --- .../cqf/cql/ls/core/utility/UrisTest.kt | 40 ++++++++++++ .../cqf/cql/ls/server/command/CqlCommand.kt | 6 +- .../server/manager/CompilerOptionsManager.kt | 3 +- .../cql/ls/server/manager/IgContextManager.kt | 3 +- .../manager/CompilerOptionsManagerTest.kt | 60 ++++++++++++++++++ .../ls/server/manager/IgContextManagerTest.kt | 62 +++++++++++++++++++ 6 files changed, 169 insertions(+), 5 deletions(-) diff --git a/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt index b08981f4..75313e15 100644 --- a/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt +++ b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt @@ -1,9 +1,11 @@ package org.opencds.cqf.cql.ls.core.utility import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.EnabledOnOs import org.junit.jupiter.api.condition.OS +import java.nio.file.Paths class UrisTest { @@ -120,6 +122,44 @@ class UrisTest { assertEquals("file:////d%3A/src/", client) } + // ----------------------------------------------------------------------- + // Paths.get(URI).toString() — the safe way to convert a file URI to a + // filesystem path string. Used in CompilerOptionsManager, IgContextManager, + // and CqlCommand to avoid the bugs below: + // + // uri.toURL().path → "/C:/foo" on Windows (leading slash, wrong) + // uri.schemeSpecificPart → "//C:/foo" on Windows (authority prefix, wrong) + // + // Platform | input URI | expected toString() + // -----------|----------------------------------|---------------------- + // macOS/Linux| file:///home/user/options.json | /home/user/options.json + // Windows | file:///C:/Users/user/options.json| C:\Users\user\options.json + // ----------------------------------------------------------------------- + + @Test + @EnabledOnOs(OS.MAC, OS.LINUX) + fun fileUriToFsPath_unix() { + val uri = Uris.parseOrNull("file:///home/user/cql/options.json")!! + val fsPath = Paths.get(uri).toString() + assertEquals("/home/user/cql/options.json", fsPath) + // Verify the two broken alternatives produce incorrect results on Unix: + // toURL().path would also work on Unix (no leading-slash bug there), but + // schemeSpecificPart includes the authority ("//") prefix. + assertFalse(uri.schemeSpecificPart == fsPath, "schemeSpecificPart should differ from the plain path") + } + + @Test + @EnabledOnOs(OS.WINDOWS) + fun fileUriToFsPath_windows() { + val uri = Uris.parseOrNull("file:///C:/Users/user/cql/options.json")!! + val fsPath = Paths.get(uri).toString() + // Expect a Windows-style path; drive letter present, no leading slash + assertEquals("C:\\Users\\user\\cql\\options.json", fsPath) + // Verify the two broken alternatives: + assertFalse(uri.toURL().path == fsPath, "toURL().path has a leading slash and is not a valid FS path on Windows") + assertFalse(uri.schemeSpecificPart == fsPath, "schemeSpecificPart is not a valid FS path on Windows") + } + @Test @EnabledOnOs(OS.WINDOWS) fun toClientStringWindows() { diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt index bfca53ae..98488194 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt @@ -156,7 +156,7 @@ class CqlCommand : Callable { } else if (parentCommand != null && rootDir != null) { npmProcessor = parentCommand!! .getIgContextManager() - .getContext(Uris.addPath(Uris.addPath(java.net.URI.create(rootDir), "input")!!, "cql")!!) + .getContext(Uris.addPath(Uris.addPath(Uris.parseOrNull(rootDir!!)!!, "input")!!, "cql")!!) if (npmProcessor != null) { igContext = npmProcessor.igContext } @@ -170,7 +170,7 @@ class CqlCommand : Callable { val optionsPathVal = optionsPath if (optionsPathVal != null) { - val op = Path(Uris.parseOrNull(optionsPathVal)!!.toURL().path) + val op = Path(Paths.get(Uris.parseOrNull(optionsPathVal)!!).toString()) val options = CqlTranslatorOptions.fromFile(Path(op)) cqlOptions.setCqlCompilerOptions(options.cqlCompilerOptions) } @@ -203,7 +203,7 @@ class CqlCommand : Callable { val libraryUrlVal = library.libraryUrl val libraryUri = if (libraryUrlVal != null) Uris.parseOrNull(libraryUrlVal) else null - val libraryKotlinPath = if (libraryUri != null) Path(libraryUri.toURL().path) else null + val libraryKotlinPath = if (libraryUri != null) Path(Paths.get(libraryUri).toString()) else null val modelPath = library.model?.modelUrl?.let { Paths.get(Uris.parseOrNull(it)!!) } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt index c3e61eb0..6e8930a6 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt @@ -1,6 +1,7 @@ package org.opencds.cqf.cql.ls.server.manager import kotlinx.io.files.Path +import java.nio.file.Paths as NioPaths import org.cqframework.cql.cql2elm.CqlCompilerOptions import org.cqframework.cql.cql2elm.CqlTranslatorOptions import org.cqframework.cql.cql2elm.LibraryBuilder.SignatureLevel @@ -38,7 +39,7 @@ class CompilerOptionsManager(private val contentService: ContentService) { options = if (input != null) { try { - CqlTranslatorOptions.fromFile(Path(optionsUri.toURL().path)) + CqlTranslatorOptions.fromFile(Path(NioPaths.get(optionsUri).toString())) .cqlCompilerOptions } catch (e: Exception) { log.info("Exception ${e.message} attempting to load options from $optionsUri, using default options") diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt index 01d7d926..10df493f 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt @@ -14,6 +14,7 @@ import org.opencds.cqf.cql.ls.core.utility.Uris import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent import org.slf4j.LoggerFactory import java.net.URI +import java.nio.file.Paths import java.util.Optional import java.util.concurrent.ConcurrentHashMap @@ -81,7 +82,7 @@ class IgContextManager(private val contentService: ContentService) { if (input != null) { log.info("Initializing ig from ini...") val igContext = IGContext(LoggerAdapter(log)) - igContext.initializeFromIni(igIniPath.schemeSpecificPart) + igContext.initializeFromIni(Paths.get(igIniPath).toString()) log.info("IGContext Initialized.") return igContext } diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt index 664926d3..256e19f9 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt @@ -5,17 +5,24 @@ import org.cqframework.cql.cql2elm.LibraryBuilder.SignatureLevel import org.eclipse.lsp4j.DidChangeWatchedFilesParams import org.eclipse.lsp4j.FileChangeType import org.eclipse.lsp4j.FileEvent +import org.hl7.elm.r1.VersionedIdentifier import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertSame import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir import org.opencds.cqf.cql.ls.core.ContentService import org.opencds.cqf.cql.ls.core.utility.Uris import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent import org.opencds.cqf.cql.ls.server.service.TestContentService +import java.io.InputStream import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths class CompilerOptionsManagerTest { private lateinit var manager: CompilerOptionsManager @@ -113,6 +120,59 @@ class CompilerOptionsManagerTest { assertSame(first, second) } + // ----------------------------------------------------------------------- + // readOptions — loads cql-options.json from filesystem via Paths.get(URI) + // + // Regression: the old code used uri.toURL().path which returns "/C:/foo" on + // Windows (leading slash), making the path invalid. The fix uses + // Paths.get(URI).toString() which produces the correct OS-native path. + // + // Platform | file URI | Paths.get().toString() + // -----------|----------------------------------------|---------------------- + // macOS/Linux| file:///tmp/test/cql/cql-options.json | /tmp/test/cql/cql-options.json + // Windows | file:///C:/tmp/test/cql/cql-options.json| C:\tmp\test\cql\cql-options.json + // ----------------------------------------------------------------------- + + @Test + fun getOptions_withRealFileOnDisk_loadsOptionsFromFile(@TempDir tempDir: Path) { + // Arrange: write a minimal (empty) cql-options.json. + // Structure: tempDir/cql/cql-options.json + // getOptions(tempDir/One.cql) → getHead → tempDir/ → readOptions → looks for tempDir/cql/cql-options.json + val cqlDir = tempDir.resolve("cql") + Files.createDirectories(cqlDir) + // Empty JSON object — no DisableListDemotion / DisableListPromotion options. + // These ARE included by CqlCompilerOptions.defaultOptions(), which readOptions uses + // when file loading fails. Their absence in the result is the distinguishing signal. + cqlDir.resolve("cql-options.json").toFile().writeText("{}") + + // ContentService that reads from the real filesystem using the file:// URI. + // This exercises the Paths.get(URI).toString() fix (regression: toURL().path gave + // "/C:/foo" on Windows with a leading slash, making the path unresolvable). + val fsContentService = object : ContentService { + override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = emptySet() + override fun read(uri: URI): InputStream? = + try { Paths.get(uri).toFile().takeIf { it.exists() }?.inputStream() } + catch (e: Exception) { null } + } + + // Act: getOptions(cqlFile) → getHead(cqlFile) = tempDir/ → readOptions(tempDir/) → + // Paths.get(optionsUri).toString() → filesystem path → CqlTranslatorOptions.fromFile + val localManager = CompilerOptionsManager(fsContentService) + val options = localManager.getOptions(tempDir.resolve("One.cql").toUri()) + + // Assert: DisableListDemotion is in CqlCompilerOptions.defaultOptions() but not in "{}". + // Its absence proves the file was successfully loaded via the fixed Paths.get(URI) path. + // If the old toURL().path bug were present, fromFile() would fail, the exception would + // be swallowed, and readOptions() would fall back to defaultOptions() — which DOES include + // DisableListDemotion. + assertNotNull(options) + assertFalse( + options.options.contains(CqlCompilerOptions.Options.DisableListDemotion), + "DisableListDemotion is only present when defaultOptions() fallback is used " + + "(i.e. file loading failed due to wrong path); its absence proves the file was read", + ) + } + companion object { // A URI whose "head" (parent path) is /org/opencds/cqf/cql/ls/server/ // TestContentService.read will be called for the cql-options.json path, returning null diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt index bcd4a677..50838fad 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt @@ -5,16 +5,23 @@ import org.eclipse.lsp4j.FileChangeType import org.eclipse.lsp4j.FileEvent import org.hl7.elm.r1.VersionedIdentifier import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledOnOs +import org.junit.jupiter.api.condition.OS +import org.junit.jupiter.api.io.TempDir import org.opencds.cqf.cql.ls.core.ContentService import org.opencds.cqf.cql.ls.core.utility.Uris import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent import org.opencds.cqf.cql.ls.server.service.TestContentService import java.io.InputStream import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths class IgContextManagerTest { private lateinit var manager: IgContextManager @@ -134,6 +141,61 @@ class IgContextManagerTest { assertEquals(countAfterFirst, readCount, "Unrelated file change should not clear cache") } + // ----------------------------------------------------------------------- + // findIgContext path conversion — the URI passed to IGContext.initializeFromIni + // must be a valid OS filesystem path, not a URI artefact. + // + // Regression: the old code used uri.schemeSpecificPart which returns + // "//path" on every platform (includes the authority prefix). + // The fix uses Paths.get(URI).toString() which returns the plain path. + // + // This test verifies the path computation that findIgContext performs using + // the exact same URI construction (Uris.addPath) used in production code. + // + // Platform | file URI | Paths.get().toString() + // -----------|---------------------------------|--------------------- + // macOS/Linux| file:///tmp/test/ig.ini | /tmp/test/ig.ini + // Windows | file:///C:/tmp/test/ig.ini | C:\tmp\test\ig.ini + // ----------------------------------------------------------------------- + + @Test + @EnabledOnOs(OS.MAC, OS.LINUX) + fun findIgContext_pathConversion_producesPlainFsPath_notSchemeSpecificPart_unix( + @TempDir tempDir: Path, + ) { + // Simulate exactly what findIgContext does: Uris.addPath(parent, "/ig.ini") + // then Paths.get(igIniPath).toString() + val parentUri = tempDir.toUri() + val igIniPath = Uris.addPath(parentUri, "/ig.ini")!! + + val fsPath = Paths.get(igIniPath).toString() + + // The old schemeSpecificPart would yield "//tmp/…"; the fix gives "/tmp/…" + assertFalse(fsPath.startsWith("//"), "schemeSpecificPart bug produces '//' prefix; got: $fsPath") + assertTrue(fsPath.startsWith("/"), "Unix path must start with '/'") + assertTrue(fsPath.endsWith("ig.ini")) + // Must be a usable filesystem path — i.e., the file can be created at it + val f = java.io.File(fsPath) + f.writeText("[IG]\n") + assertTrue(f.exists(), "File should be creatable at the converted path") + } + + @Test + @EnabledOnOs(OS.WINDOWS) + fun findIgContext_pathConversion_producesPlainFsPath_notSchemeSpecificPart_windows( + @TempDir tempDir: Path, + ) { + val parentUri = tempDir.toUri() + val igIniPath = Uris.addPath(parentUri, "/ig.ini")!! + + val fsPath = Paths.get(igIniPath).toString() + + // Windows path starts with a drive letter (e.g. "C:\"), not "//" or "/" + assertFalse(fsPath.startsWith("//"), "schemeSpecificPart bug produces '//' prefix; got: $fsPath") + assertFalse(fsPath.startsWith("/"), "Windows path should not start with '/'") + assertTrue(fsPath.endsWith("ig.ini")) + } + companion object { // A URI whose parent dir is /org/opencds/cqf/cql/ls/server/ — // TestContentService returns null for the ig.ini probe, so NpmProcessor is never created. From b4f81c47b53b5200e89d7d789941f1dd1675bd9b Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Sat, 21 Mar 2026 01:14:14 -0400 Subject: [PATCH 08/19] spotless apply --- .../manager/CompilerOptionsManagerTest.kt | 24 +++++++++++++------ .../ls/server/manager/IgContextManagerTest.kt | 1 - 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt index 256e19f9..bb1ceb4e 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManagerTest.kt @@ -134,7 +134,9 @@ class CompilerOptionsManagerTest { // ----------------------------------------------------------------------- @Test - fun getOptions_withRealFileOnDisk_loadsOptionsFromFile(@TempDir tempDir: Path) { + fun getOptions_withRealFileOnDisk_loadsOptionsFromFile( + @TempDir tempDir: Path, + ) { // Arrange: write a minimal (empty) cql-options.json. // Structure: tempDir/cql/cql-options.json // getOptions(tempDir/One.cql) → getHead → tempDir/ → readOptions → looks for tempDir/cql/cql-options.json @@ -148,12 +150,20 @@ class CompilerOptionsManagerTest { // ContentService that reads from the real filesystem using the file:// URI. // This exercises the Paths.get(URI).toString() fix (regression: toURL().path gave // "/C:/foo" on Windows with a leading slash, making the path unresolvable). - val fsContentService = object : ContentService { - override fun locate(root: URI, libraryIdentifier: VersionedIdentifier): Set = emptySet() - override fun read(uri: URI): InputStream? = - try { Paths.get(uri).toFile().takeIf { it.exists() }?.inputStream() } - catch (e: Exception) { null } - } + val fsContentService = + object : ContentService { + override fun locate( + root: URI, + libraryIdentifier: VersionedIdentifier, + ): Set = emptySet() + + override fun read(uri: URI): InputStream? = + try { + Paths.get(uri).toFile().takeIf { it.exists() }?.inputStream() + } catch (e: Exception) { + null + } + } // Act: getOptions(cqlFile) → getHead(cqlFile) = tempDir/ → readOptions(tempDir/) → // Paths.get(optionsUri).toString() → filesystem path → CqlTranslatorOptions.fromFile diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt index 50838fad..199705b8 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt @@ -19,7 +19,6 @@ import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent import org.opencds.cqf.cql.ls.server.service.TestContentService import java.io.InputStream import java.net.URI -import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths From 9aa64f55dd212efb9705ad089f212ef45e404dad Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Sat, 21 Mar 2026 01:19:10 -0400 Subject: [PATCH 09/19] fixes issue with windows stream not closing --- .../opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt index 6e8930a6..b7705a30 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt @@ -36,6 +36,7 @@ class CompilerOptionsManager(private val contentService: ContentService) { val optionsUri = Uris.addPath(rootUri, "/cql/cql-options.json") val input = contentService.read(optionsUri!!) + input?.close() options = if (input != null) { try { From 51a2d5db093ca25c1fdd3a472f8f9aaec47c9b18 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Sun, 22 Mar 2026 16:31:29 -0400 Subject: [PATCH 10/19] fixes edge case with setTrace --- .../java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.kt index 15df98f3..61e4938c 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/CqlLanguageServer.kt @@ -4,6 +4,7 @@ import org.eclipse.lsp4j.InitializeParams import org.eclipse.lsp4j.InitializeResult import org.eclipse.lsp4j.InitializedParams import org.eclipse.lsp4j.ServerCapabilities +import org.eclipse.lsp4j.SetTraceParams import org.eclipse.lsp4j.services.LanguageClient import org.eclipse.lsp4j.services.LanguageClientAware import org.eclipse.lsp4j.services.LanguageServer @@ -46,6 +47,11 @@ class CqlLanguageServer( // Nothing to do, currently. } + override fun setTrace(params: SetTraceParams) { + // No-op: VS Code sends $/setTrace on startup; suppress the UnsupportedOperationException + // from the LSP4J default implementation. + } + override fun shutdown(): CompletableFuture { // Nothing to do currently return CompletableFuture.completedFuture(null) From 29a04cf9c7ef0ab7412c06f622fb10bac1202277 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 23 Mar 2026 10:32:02 -0400 Subject: [PATCH 11/19] adds more tests --- .../cqf/cql/ls/core/ContentServiceTest.kt | 99 ++++++++ .../cqf/cql/ls/server/command/CqlCommand.kt | 7 +- .../cql/ls/server/command/CqlCommandTest.kt | 219 ++++++++++++++++++ .../DebugCqlCommandContributionTest.kt | 126 ++++++++++ .../ls/server/manager/IgContextManagerTest.kt | 149 ++++++++++++ .../service/ActiveContentServiceTest.kt | 171 ++++++++++++++ .../opencds/cqf/cql/ls/server/NullResult.cql | 4 + .../debug/DebugCommandContributionTest.kt | 81 +++++++ 8 files changed, 855 insertions(+), 1 deletion(-) create mode 100644 core/src/test/java/org/opencds/cqf/cql/ls/core/ContentServiceTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContributionTest.kt create mode 100644 ls/server/src/test/resources/org/opencds/cqf/cql/ls/server/NullResult.cql create mode 100644 plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContributionTest.kt diff --git a/core/src/test/java/org/opencds/cqf/cql/ls/core/ContentServiceTest.kt b/core/src/test/java/org/opencds/cqf/cql/ls/core/ContentServiceTest.kt new file mode 100644 index 00000000..e6eb477c --- /dev/null +++ b/core/src/test/java/org/opencds/cqf/cql/ls/core/ContentServiceTest.kt @@ -0,0 +1,99 @@ +package org.opencds.cqf.cql.ls.core + +import org.apache.commons.lang3.NotImplementedException +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.io.InputStream +import java.net.URI +import java.nio.file.Files + +class ContentServiceTest { + + private fun id(name: String, version: String? = null): VersionedIdentifier = + VersionedIdentifier().withId(name).let { if (version != null) it.withVersion(version) else it } + + private val root = URI("file:///workspace/root/") + + // ------------------------------------------------------------------------- + // locate() — default implementation + // ------------------------------------------------------------------------- + + @Test + fun `locate default throws NotImplementedException`() { + val cs = object : ContentService {} + assertThrows { cs.locate(root, id("Foo")) } + } + + // ------------------------------------------------------------------------- + // read(root, identifier) — template-method behaviour + // ------------------------------------------------------------------------- + + @Test + fun `read returns null when locate finds no locations`() { + val cs = object : ContentService { + override fun locate(root: URI, identifier: VersionedIdentifier) = emptySet() + } + assertNull(cs.read(root, id("Foo"))) + } + + @Test + fun `read delegates to read(uri) when locate finds a single location`() { + val targetUri = URI("file:///workspace/root/Foo.cql") + val sentinel: InputStream = "sentinel".byteInputStream() + val cs = object : ContentService { + override fun locate(root: URI, identifier: VersionedIdentifier) = setOf(targetUri) + override fun read(uri: URI) = if (uri == targetUri) sentinel else null + } + assertSame(sentinel, cs.read(root, id("Foo"))) + } + + @Test + fun `read throws IllegalStateException when locate finds multiple locations`() { + val cs = object : ContentService { + override fun locate(root: URI, identifier: VersionedIdentifier) = + setOf(URI("file:///a/Foo.cql"), URI("file:///b/Foo.cql")) + } + assertThrows { cs.read(root, id("Foo")) } + } + + @Test + fun `IllegalStateException message includes library id and version`() { + val cs = object : ContentService { + override fun locate(root: URI, identifier: VersionedIdentifier) = + setOf(URI("file:///a/MyLib.cql"), URI("file:///b/MyLib.cql")) + } + val ex = assertThrows { cs.read(root, id("MyLib", "2.3.0")) } + assertTrue(ex.message!!.contains("MyLib"), "message should include library id") + assertTrue(ex.message!!.contains("2.3.0"), "message should include library version") + } + + // ------------------------------------------------------------------------- + // read(uri) — default implementation + // ------------------------------------------------------------------------- + + @Test + fun `read(uri) opens stream for a valid file URI`() { + val tempFile = Files.createTempFile("content-service-test", ".cql") + try { + Files.write(tempFile, "library Test".toByteArray()) + val cs = object : ContentService {} + val stream = cs.read(tempFile.toUri()) + assertNotNull(stream) + stream!!.close() + } finally { + Files.deleteIfExists(tempFile) + } + } + + @Test + fun `read(uri) returns null for a non-existent file URI`() { + val cs = object : ContentService {} + val uri = URI("file:///nonexistent/path/that/does/not/exist/Foo.cql") + assertNull(cs.read(uri)) + } +} diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt index 98488194..ec10960e 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt @@ -236,7 +236,12 @@ class CqlCommand : Callable { org.apache.commons.lang3.tuple.Pair.of(library.context!!.contextName, library.context!!.contextValue as Any?) } else null - val result = engine.evaluate(identifier, contextParameter) + val expressions = library.expression?.toSet() + val result = if (expressions != null) { + engine.evaluate(identifier, expressions, contextParameter) + } else { + engine.evaluate(identifier, contextParameter) + } writeResult(result) } diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt new file mode 100644 index 00000000..dfb42118 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt @@ -0,0 +1,219 @@ +package org.opencds.cqf.cql.ls.server.command + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.junit.jupiter.api.assertThrows +import org.opencds.cqf.cql.ls.core.utility.Uris +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class CqlCommandTest { + private lateinit var originalOut: PrintStream + private lateinit var capturedOut: ByteArrayOutputStream + + @BeforeEach + fun captureStdout() { + originalOut = System.out + capturedOut = ByteArrayOutputStream() + System.setOut(PrintStream(capturedOut)) + } + + @AfterEach + fun restoreStdout() { + System.setOut(originalOut) + } + + private fun output(): String = capturedOut.toString() + + /** + * Returns a file: URI pointing to the directory containing the CQL test fixtures. + * + * Uses .toURI() (not URI.create()) so the path is correct on all platforms: + * macOS/Linux: file:///home/user/... + * Windows: file:///C:/Users/... + */ + private fun cqlFixtureDirectoryUrl(): String = + CqlCommandTest::class.java + .getResource("/org/opencds/cqf/cql/ls/server/One.cql")!! + .toURI() + .resolve(".") + .toString() + + private fun buildCommand( + libraryName: String, + libraryVersion: String? = null, + expressions: Array? = null, + fhirVersion: String = "R4", + ): CqlCommand { + val cmd = CqlCommand() + cmd.fhirVersion = fhirVersion + val lib = CqlCommand.LibraryParameter() + lib.libraryUrl = cqlFixtureDirectoryUrl() + lib.libraryName = libraryName + lib.libraryVersion = libraryVersion + lib.expression = expressions + cmd.libraries = mutableListOf(lib) + return cmd + } + + // ------------------------------------------------------------------------- + // Tests 1–12: execution paths, output formatting, and error cases + // ------------------------------------------------------------------------- + + @Test + @Order(1) + fun `happy path - One evaluates One=1`() { + val cmd = buildCommand("One") + val result = cmd.call() + assertEquals(0, result) + assertTrue(output().contains("One=1")) + } + + @Test + @Order(2) + fun `happy path - Two with version 1-0-0 evaluates Two=2`() { + val cmd = buildCommand("Two", libraryVersion = "1.0.0") + val result = cmd.call() + assertEquals(0, result) + assertTrue(output().contains("Two=2")) + } + + @Test + @Order(3) + fun `expression filter - only requested expression appears`() { + val cmd = buildCommand("Two", expressions = arrayOf("Two")) + cmd.call() + val out = output() + assertTrue(out.contains("Two=2"), "Expected 'Two=2' in output") + assertFalse(out.contains("Two List"), "Expected 'Two List' to be filtered out when -e Two is set") + } + + @Test + @Order(4) + fun `no expression filter - all defines appear`() { + val cmd = buildCommand("Two") + cmd.call() + val out = output() + assertTrue(out.contains("Two=2")) + assertTrue(out.contains("Two List")) + } + + @Test + @Order(5) + fun `Two List is formatted as bracket list`() { + val cmd = buildCommand("Two") + cmd.call() + assertTrue(output().contains("Two List=[1, 2, 3]")) + } + + @Test + @Order(6) + fun `blank line appears after expression results`() { + val cmd = buildCommand("One") + cmd.call() + // writeResult() calls println() after iterating results, producing a trailing blank line + val out = output() + assertTrue(out.contains("\n\n") || out.endsWith("\n\n")) + } + + @Test + @Order(7) + fun `null value is rendered as string null`() { + val cmd = buildCommand("NullResult") + cmd.call() + assertTrue(output().contains("NullDef=null")) + } + + @Test + @Order(8) + fun `call returns 0 on success`() { + val cmd = buildCommand("One") + assertEquals(0, cmd.call()) + } + + @Test + @Order(9) + fun `NoOpRepository path - evaluation succeeds without terminology or model`() { + // buildCommand sets no terminologyUrl or modelUrl, so createRepository returns NoOpRepository + val cmd = buildCommand("One") + val result = cmd.call() + assertEquals(0, result) + assertTrue(output().contains("One=1")) + } + + @Test + @Order(10) + fun `R5 fhir version - evaluation succeeds`() { + val cmd = buildCommand("One", fhirVersion = "R5") + val result = cmd.call() + assertEquals(0, result) + assertTrue(output().contains("One=1")) + } + + @Test + @Order(11) + fun `DSTU3 fhir version - evaluation succeeds`() { + val cmd = buildCommand("One", fhirVersion = "DSTU3") + val result = cmd.call() + assertEquals(0, result) + assertTrue(output().contains("One=1")) + } + + @Test + @Order(12) + fun `invalid fhir version throws IllegalArgumentException`() { + val cmd = buildCommand("One", fhirVersion = "INVALID") + assertThrows { cmd.call() } + } + + // ------------------------------------------------------------------------- + // Tests 13–15: cross-platform path handling via Uris.parseOrNull() + // No CQL engine involvement — pure unit tests of URI parsing behaviour. + // + // Platform behaviour matrix: + // Format | Example | parseOrNull result + // ------------------------------|-------------------------------|------------------- + // Unix / macOS | file:///tmp/cql/ | non-null URI + // Windows forward-slash | file:///C:/Users/cql/ | non-null URI (any platform) + // Windows backslash | file:///C:\Users\cql\ | null (URISyntaxException) + // + // The backslash case documents a latent NPE in CqlCommand: libraryKotlinPath!! will + // throw NullPointerException if a backslash path reaches libraryUrl. + // ------------------------------------------------------------------------- + + @Test + @Order(13) + fun `unix style file URI parses to valid path`() { + val uri = Uris.parseOrNull("file:///tmp/cql/") + assertNotNull(uri) + assertEquals("file", uri!!.scheme) + } + + @Test + @Order(14) + fun `windows forward slash URI parses to valid URI`() { + val uri = Uris.parseOrNull("file:///C:/Users/cql/") + assertNotNull(uri) + assertEquals("file", uri!!.scheme) + } + + @Test + @Order(15) + fun `windows backslash URI returns null from parseOrNull`() { + // Backslashes are illegal in URIs per RFC 3986; URI(String) throws URISyntaxException, + // which parseOrNull catches and converts to null. This documents the latent NPE risk: + // CqlCommand line `libraryKotlinPath!!` will throw if this reaches libraryUrl. + val uri = Uris.parseOrNull("file:///C:\\Users\\cql\\") + assertNull(uri, "Backslash paths are not valid URIs; parseOrNull should return null") + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContributionTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContributionTest.kt new file mode 100644 index 00000000..a75d17a7 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContributionTest.kt @@ -0,0 +1,126 @@ +package org.opencds.cqf.cql.ls.server.command + +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import org.eclipse.lsp4j.ExecuteCommandParams +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotSame +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.opencds.cqf.cql.ls.server.command.DebugCqlCommandContribution.Companion.START_DEBUG_COMMAND +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import org.opencds.cqf.cql.ls.server.service.TestContentService +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +class DebugCqlCommandContributionTest { + companion object { + private lateinit var contribution: DebugCqlCommandContribution + + @BeforeAll + @JvmStatic + fun beforeAll() { + val cs = TestContentService() + contribution = DebugCqlCommandContribution(IgContextManager(cs)) + } + } + + /** + * Returns a file: URI pointing to the directory containing the CQL test fixtures. + * Uses .toURI() so the path is correct on macOS/Linux and Windows. + */ + private fun cqlFixtureDirectoryUrl(): String = + DebugCqlCommandContributionTest::class.java + .getResource("/org/opencds/cqf/cql/ls/server/One.cql")!! + .toURI() + .resolve(".") + .toString() + + /** Wraps each string as a JsonPrimitive, matching the format executeCql() expects. */ + private fun jsonArgs(vararg args: String): List = args.map { JsonPrimitive(it) } + + private fun debugParams(vararg args: String): ExecuteCommandParams = ExecuteCommandParams(START_DEBUG_COMMAND, jsonArgs(*args)) + + // ------------------------------------------------------------------------- + // Command registration + // ------------------------------------------------------------------------- + + @Test + fun `getCommands returns the start debug session command`() { + assertEquals(setOf(START_DEBUG_COMMAND), contribution.getCommands()) + } + + // ------------------------------------------------------------------------- + // Dispatch + // ------------------------------------------------------------------------- + + @Test + fun `executeCommand with unknown command throws RuntimeException`() { + val params = ExecuteCommandParams("org.unknown.command", emptyList()) + assertThrows { contribution.executeCommand(params) } + } + + // ------------------------------------------------------------------------- + // Successful execution + // ------------------------------------------------------------------------- + + @Test + fun `successful evaluation returns CQL output in the future result`() { + val params = debugParams("cql", "-fv", "R4", "-lu", cqlFixtureDirectoryUrl(), "-ln", "One") + val result = contribution.executeCommand(params).join() as String + assertTrue(result.contains("One=1")) + } + + @Test + fun `CQL output does not bleed to caller System dot out`() { + // executeCql() redirects System.out internally; output should go to the future result, + // not to whatever System.out the caller had set. + val callerCapture = ByteArrayOutputStream() + val originalOut = System.out + System.setOut(PrintStream(callerCapture)) + try { + val params = debugParams("cql", "-fv", "R4", "-lu", cqlFixtureDirectoryUrl(), "-ln", "One") + val result = contribution.executeCommand(params).join() as String + assertEquals("", callerCapture.toString(), "CQL output should not appear in caller's stdout") + assertTrue(result.contains("One=1"), "CQL output should be in the future result") + } finally { + System.setOut(originalOut) + } + } + + @Test + fun `stdout and stderr are restored to console streams after successful execution`() { + val preCallOut = System.out + val params = debugParams("cql", "-fv", "R4", "-lu", cqlFixtureDirectoryUrl(), "-ln", "One") + contribution.executeCommand(params).join() + // finally block always replaces System.out with new PrintStream(FileDescriptor.out) + assertNotSame(preCallOut, System.out, "stdout should be replaced with a new console stream") + assertDoesNotThrow { System.out.println("stdout is functional after execution") } + } + + // ------------------------------------------------------------------------- + // Failed execution + // ------------------------------------------------------------------------- + + @Test + fun `failed evaluation includes Evaluation logs section in result`() { + // "NonExistentLibrary" is not present in the fixture directory; the engine will throw, + // picocli will write the error to the redirected System.err, and executeCql() appends + // it to the result under "Evaluation logs:". + val params = debugParams("cql", "-fv", "R4", "-lu", cqlFixtureDirectoryUrl(), "-ln", "NonExistentLibrary") + val result = contribution.executeCommand(params).join() as String + assertTrue(result.contains("Evaluation logs:"), "Expected 'Evaluation logs:' section when CQL evaluation fails") + } + + @Test + fun `stdout and stderr are restored after failed evaluation`() { + val params = debugParams("cql", "-fv", "R4", "-lu", cqlFixtureDirectoryUrl(), "-ln", "NonExistentLibrary") + contribution.executeCommand(params).join() + // finally block runs even on error paths + assertDoesNotThrow { System.out.println("stdout still functional after failure") } + assertDoesNotThrow { System.err.println("stderr still functional after failure") } + } +} diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt index 199705b8..14fbe734 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt @@ -1,5 +1,7 @@ package org.opencds.cqf.cql.ls.server.manager +import org.cqframework.cql.cql2elm.LibraryManager +import org.cqframework.cql.cql2elm.ModelManager import org.eclipse.lsp4j.DidChangeWatchedFilesParams import org.eclipse.lsp4j.FileChangeType import org.eclipse.lsp4j.FileEvent @@ -10,6 +12,7 @@ import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.condition.EnabledOnOs import org.junit.jupiter.api.condition.OS import org.junit.jupiter.api.io.TempDir @@ -195,6 +198,152 @@ class IgContextManagerTest { assertTrue(fsPath.endsWith("ig.ini")) } + // ----------------------------------------------------------------------- + // Caching — scope of cache key is the root (parent) URI, not the file URI + // ----------------------------------------------------------------------- + + @Test + fun getContext_twoUrisInSameDirectory_readsContentServiceOnce() { + var readCount = 0 + val cs = + object : ContentService { + override fun locate( + root: URI, + identifier: VersionedIdentifier, + ) = emptySet() + + override fun read(uri: URI): InputStream? { + readCount++ + return null + } + } + val localManager = IgContextManager(cs) + val dir = "file:///workspace/cql/" + localManager.getContext(Uris.parseOrNull("${dir}One.cql")!!) + val after1 = readCount + // Second URI is in the same directory → same root key → should be cached + localManager.getContext(Uris.parseOrNull("${dir}Two.cql")!!) + assertEquals(after1, readCount, "Second URI in the same directory should reuse the cached entry") + } + + @Test + fun getContext_twoUrisInDifferentDirectories_cachesIndependently() { + var readCount = 0 + val cs = + object : ContentService { + override fun locate( + root: URI, + identifier: VersionedIdentifier, + ) = emptySet() + + override fun read(uri: URI): InputStream? { + readCount++ + return null + } + } + val localManager = IgContextManager(cs) + localManager.getContext(Uris.parseOrNull("file:///workspace/lib1/One.cql")!!) + val after1 = readCount + localManager.getContext(Uris.parseOrNull("file:///workspace/lib2/Two.cql")!!) + assertTrue(readCount > after1, "Different directories should produce independent cache entries") + } + + // ----------------------------------------------------------------------- + // setupLibraryManager — no ig context → early return, no modification + // ----------------------------------------------------------------------- + + @Test + fun setupLibraryManager_noIgContext_doesNotThrow() { + // getContext returns null for TEST_URI (no ig.ini in classpath). + // setupLibraryManager should return early without touching libraryManager. + val libraryManager = LibraryManager(ModelManager()) + assertDoesNotThrow { manager.setupLibraryManager(TEST_URI, libraryManager) } + } + + // ----------------------------------------------------------------------- + // findIgContext depth limit — only searches 2 parent levels + // ----------------------------------------------------------------------- + + @Test + fun getContext_igIniOnlyAtThirdParentLevel_returnsNull() { + // For URI file:///workspace/a/b/c/d.cql: + // root passed to findIgContext = file:///workspace/a/b/c + // i=0: parent = file:///workspace/a/b → probes file:///workspace/a/b/ig.ini + // i=1: parent = file:///workspace/a → probes file:///workspace/a/ig.ini + // NOT probed: file:///workspace/ig.ini (would require a 3rd iteration) + // + // Providing non-null only for file:///workspace/ig.ini should yield null. + val tooDeepIgIni = Uris.parseOrNull("file:///workspace/ig.ini")!! + val cs = + object : ContentService { + override fun locate( + root: URI, + identifier: VersionedIdentifier, + ) = emptySet() + + override fun read(uri: URI): InputStream? = if (uri == tooDeepIgIni) "content".byteInputStream() else null + } + val localManager = IgContextManager(cs) + val result = localManager.getContext(Uris.parseOrNull("file:///workspace/a/b/c/d.cql")!!) + assertNull(result, "ig.ini beyond the 2-level search depth should not be found") + } + + // ----------------------------------------------------------------------- + // onMessageEvent — partial cache invalidation + // ----------------------------------------------------------------------- + + @Test + fun onMessageEvent_igIniChangeForOneRoot_doesNotClearOtherRootsCache() { + // Use 3-segment URIs so that findIgContext probes paths within each root's subtree. + // For file:///workspace/root1/cql/One.cql: + // cache key (root) = file:///workspace/root1/cql + // findIgContext probes file:///workspace/root1/ig.ini ← counted in root1Reads + // + // The ig.ini event URI must have its "head" equal to the cache key so that + // clearContext removes the right entry: + // clearContext(file:///workspace/root1/cql/ig.ini) + // → Uris.getHead(...) = file:///workspace/root1/cql ← matches cache key ✓ + val root1Reads = mutableListOf() + val root2Reads = mutableListOf() + val cs = + object : ContentService { + override fun locate( + root: URI, + identifier: VersionedIdentifier, + ) = emptySet() + + override fun read(uri: URI): InputStream? { + if (uri.toString().startsWith("file:///workspace/root1")) { + root1Reads.add(uri) + } else if (uri.toString().startsWith("file:///workspace/root2")) { + root2Reads.add(uri) + } + return null + } + } + val localManager = IgContextManager(cs) + val root1Uri = Uris.parseOrNull("file:///workspace/root1/cql/One.cql")!! + val root2Uri = Uris.parseOrNull("file:///workspace/root2/cql/Two.cql")!! + + localManager.getContext(root1Uri) // caches root1 + localManager.getContext(root2Uri) // caches root2 + val root1ReadsBefore = root1Reads.size + val root2ReadsBefore = root2Reads.size + + // Clear only root1's cache by sending an ig.ini event for root1/cql/ig.ini + val root1IgIniEvent = "file:///workspace/root1/cql/ig.ini" + localManager.onMessageEvent( + DidChangeWatchedFilesEvent( + DidChangeWatchedFilesParams(listOf(FileEvent(root1IgIniEvent, FileChangeType.Changed))), + ), + ) + + localManager.getContext(root1Uri) // cache cleared → re-reads root1 + localManager.getContext(root2Uri) // still cached → no extra reads for root2 + assertTrue(root1Reads.size > root1ReadsBefore, "root1 cache should be cleared by its ig.ini change") + assertEquals(root2ReadsBefore, root2Reads.size, "root2 cache should not be affected") + } + companion object { // A URI whose parent dir is /org/opencds/cqf/cql/ls/server/ — // TestContentService returns null for the ig.ini probe, so NpmProcessor is never created. diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.kt index a812eb40..508ac4ea 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/ActiveContentServiceTest.kt @@ -13,8 +13,11 @@ import org.hl7.elm.r1.VersionedIdentifier import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows import org.opencds.cqf.cql.ls.core.utility.Uris import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent @@ -169,6 +172,174 @@ class ActiveContentServiceTest { assertFalse(found.contains(DOC_URI_PARSED)) } + // ----------------------------------------------------------------------- + // read(URI) + // ----------------------------------------------------------------------- + + @Test + fun read_uri_returnsNullForUnknownUri() { + val svc = ActiveContentService() + assertNull(svc.read(DOC_URI_PARSED)) + } + + @Test + fun read_uri_returnsActiveContentAsStream() { + val svc = openDoc(LIBRARY_CONTENT, 1) + val content = svc.read(DOC_URI_PARSED)!!.readAllBytes().toString(Charsets.UTF_8) + assertEquals(LIBRARY_CONTENT, content) + } + + // ----------------------------------------------------------------------- + // read(root, identifier) + // ----------------------------------------------------------------------- + + @Test + fun read_rootId_throwsWhenNoDocumentMatchesIdentifier() { + val svc = ActiveContentService() // nothing open + val id = VersionedIdentifier().withId("One").withVersion("1.0.0") + assertThrows { svc.read(ROOT, id) } + } + + @Test + fun read_rootId_returnsStreamForSingleMatchingDocument() { + val svc = openDoc(LIBRARY_CONTENT, 1) + val id = VersionedIdentifier().withId("One").withVersion("1.0.0") + val content = svc.read(ROOT, id)!!.readAllBytes().toString(Charsets.UTF_8) + assertEquals(LIBRARY_CONTENT, content) + } + + @Test + fun read_rootId_throwsWhenMultipleDocumentsMatch() { + val svc = openDoc(LIBRARY_CONTENT, 1) + // Open a second URI with the same library content so locate() returns two results + val params2 = DidOpenTextDocumentParams() + val item2 = TextDocumentItem() + item2.uri = "file:///workspace/OneCopy.cql" + item2.text = LIBRARY_CONTENT + item2.version = 1 + params2.textDocument = item2 + svc.didOpen(DidOpenTextDocumentEvent(params2)) + + val id = VersionedIdentifier().withId("One").withVersion("1.0.0") + assertThrows { svc.read(ROOT, id) } + } + + // ----------------------------------------------------------------------- + // didChange — additional branches + // ----------------------------------------------------------------------- + + @Test + fun didChange_rangeBased_appliesPatch() { + val svc = openDoc("hello world", 1) + + val ce = TextDocumentContentChangeEvent() + ce.range = Range(Position(0, 6), Position(0, 11)) + ce.text = "CQL" + ce.rangeLength = 5 + + val change = DidChangeTextDocumentParams() + change.textDocument = VersionedTextDocumentIdentifier(DOC_URI, 2) + change.contentChanges = listOf(ce) + svc.didChange(DidChangeTextDocumentEvent(change)) + + val content = svc.read(DOC_URI_PARSED)!!.readAllBytes().toString(Charsets.UTF_8) + assertEquals("hello CQL", content) + } + + @Test + fun didChange_sameVersion_contentUnchanged() { + val svc = openDoc("original", 5) + + // version == existing.version — not strictly greater, so change is ignored + val change = DidChangeTextDocumentParams() + change.textDocument = VersionedTextDocumentIdentifier(DOC_URI, 5) + change.contentChanges = listOf(TextDocumentContentChangeEvent("should be ignored")) + svc.didChange(DidChangeTextDocumentEvent(change)) + + val content = svc.read(DOC_URI_PARSED)!!.readAllBytes().toString(Charsets.UTF_8) + assertEquals("original", content) + } + + @Test + fun didChange_noopForUntrackedUri() { + val svc = ActiveContentService() // nothing open + val change = DidChangeTextDocumentParams() + change.textDocument = VersionedTextDocumentIdentifier(DOC_URI, 1) + change.contentChanges = listOf(TextDocumentContentChangeEvent("irrelevant")) + assertDoesNotThrow { svc.didChange(DidChangeTextDocumentEvent(change)) } + assertFalse(svc.activeUris().contains(DOC_URI_PARSED)) + } + + // ----------------------------------------------------------------------- + // Invalid URI handling — all three events must not throw + // ----------------------------------------------------------------------- + + @Test + fun didOpen_malformedUri_doesNotCrash() { + val svc = ActiveContentService() + val params = DidOpenTextDocumentParams() + val item = TextDocumentItem() + item.uri = "not a valid uri with spaces" + item.text = LIBRARY_CONTENT + item.version = 1 + params.textDocument = item + assertDoesNotThrow { svc.didOpen(DidOpenTextDocumentEvent(params)) } + assertTrue(svc.activeUris().isEmpty()) + } + + @Test + fun didClose_malformedUri_doesNotCrash() { + val svc = openDoc(LIBRARY_CONTENT, 1) + val close = DidCloseTextDocumentParams() + close.textDocument = TextDocumentIdentifier("not a valid uri with spaces") + assertDoesNotThrow { svc.didClose(DidCloseTextDocumentEvent(close)) } + // original document still tracked + assertTrue(svc.activeUris().contains(DOC_URI_PARSED)) + } + + @Test + fun didChange_malformedUri_doesNotCrash() { + val svc = ActiveContentService() + val change = DidChangeTextDocumentParams() + change.textDocument = VersionedTextDocumentIdentifier("not a valid uri with spaces", 1) + change.contentChanges = listOf(TextDocumentContentChangeEvent("text")) + assertDoesNotThrow { svc.didChange(DidChangeTextDocumentEvent(change)) } + } + + // ----------------------------------------------------------------------- + // searchActiveContent — null version + // ----------------------------------------------------------------------- + + @Test + fun searchActiveContent_nullVersion_doesNotMatchVersionedLibrary() { + // When identifier.version == null the regex is: (?s).*library\s+{id}'\s+(?s).* + // This requires a literal ' immediately after the library name, so it does NOT + // match a standard "library One version '1.0.0'" declaration. + val svc = openDoc(LIBRARY_CONTENT, 1) + val id = VersionedIdentifier().withId("One") // no version + val found = svc.searchActiveContent(ROOT, id) + assertTrue(found.isEmpty(), "null-version search should not match a versioned library declaration") + } + + // ----------------------------------------------------------------------- + // patch — additional cases + // ----------------------------------------------------------------------- + + @Test + @Suppress("DEPRECATION") + fun patch_deletion_removesSelectedRange() { + val svc = ActiveContentService() + + val change = TextDocumentContentChangeEvent() + change.range = Range(Position(0, 5), Position(0, 11)) // selects " world" + change.text = "" + change.rangeLength = 6 + + val result = svc.patch("hello world", change) + + assertEquals("hello", result) + } + companion object { private const val DOC_URI = "file:///workspace/One.cql" private val DOC_URI_PARSED: URI = Uris.parseOrNull(DOC_URI)!! diff --git a/ls/server/src/test/resources/org/opencds/cqf/cql/ls/server/NullResult.cql b/ls/server/src/test/resources/org/opencds/cqf/cql/ls/server/NullResult.cql new file mode 100644 index 00000000..dc88c37b --- /dev/null +++ b/ls/server/src/test/resources/org/opencds/cqf/cql/ls/server/NullResult.cql @@ -0,0 +1,4 @@ +library NullResult + +define "NullDef": + null diff --git a/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContributionTest.kt b/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContributionTest.kt new file mode 100644 index 00000000..e0543573 --- /dev/null +++ b/plugin/debug/src/test/java/org/opencds/cqf/cql/ls/plugin/debug/DebugCommandContributionTest.kt @@ -0,0 +1,81 @@ +package org.opencds.cqf.cql.ls.plugin.debug + +import org.eclipse.lsp4j.ExecuteCommandParams +import org.hl7.elm.r1.VersionedIdentifier +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.opencds.cqf.cql.ls.core.ContentService +import org.opencds.cqf.cql.ls.plugin.debug.DebugCommandContribution.Companion.START_DEBUG_COMMAND +import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import java.io.InputStream +import java.net.URI + +class DebugCommandContributionTest { + + private lateinit var contribution: DebugCommandContribution + + @BeforeEach + fun setUp() { + // cqlCompilationManager is injected but not referenced inside executeCommand; + // construct it with a no-op ContentService to keep the build lightweight. + val cs = object : ContentService { + override fun locate(root: URI, identifier: VersionedIdentifier) = emptySet() + override fun read(uri: URI): InputStream? = null + } + val cm = CqlCompilationManager(cs, CompilerOptionsManager(cs), IgContextManager(cs)) + contribution = DebugCommandContribution(cm) + } + + // ------------------------------------------------------------------------- + // Command registration + // ------------------------------------------------------------------------- + + @Test + fun `getCommands returns the start debug session command`() { + assertEquals(setOf(START_DEBUG_COMMAND), contribution.getCommands()) + } + + // ------------------------------------------------------------------------- + // Dispatch — unknown command + // ------------------------------------------------------------------------- + + @Test + fun `executeCommand with unknown command throws IllegalArgumentException`() { + val params = ExecuteCommandParams("org.unknown.command", emptyList()) + // DebugCommandContribution throws IllegalArgumentException (not RuntimeException) + // for unrecognised commands, unlike the CommandContribution interface default. + assertThrows { contribution.executeCommand(params) } + } + + // ------------------------------------------------------------------------- + // Start debug session — happy path + // ------------------------------------------------------------------------- + + @Test + fun `executeCommand starts a new session and returns a positive port number`() { + val params = ExecuteCommandParams(START_DEBUG_COMMAND, emptyList()) + // executeCommand blocks until DebugSession.start() resolves (ServerSocket bound). + val result = contribution.executeCommand(params).join() + assertTrue(result is Int, "result should be a port number (Int)") + assertTrue((result as Int) > 0, "port should be a positive integer") + } + + // ------------------------------------------------------------------------- + // Start debug session — already active + // ------------------------------------------------------------------------- + + @Test + fun `executeCommand throws IllegalStateException when a session is already active`() { + val params = ExecuteCommandParams(START_DEBUG_COMMAND, emptyList()) + // First call: starts a new session and waits for the port to be assigned. + // After this returns, isActive() == true (flag is set before socket starts). + contribution.executeCommand(params).join() + // Second call: finds session != null && session.isActive() → throws + assertThrows { contribution.executeCommand(params) } + } +} From 1d91a2b7d96eafc0a2a014ebe96844484370a7da Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 23 Mar 2026 11:09:40 -0400 Subject: [PATCH 12/19] fixes issues with Windows tests --- .../cqf/cql/ls/server/command/CqlCommandTest.kt | 6 ++++-- .../cqf/cql/ls/server/manager/IgContextManagerTest.kt | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt index dfb42118..b86ce0cb 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt @@ -121,9 +121,11 @@ class CqlCommandTest { fun `blank line appears after expression results`() { val cmd = buildCommand("One") cmd.call() - // writeResult() calls println() after iterating results, producing a trailing blank line + // writeResult() calls println() after iterating results, producing a trailing blank line. + // Use System.lineSeparator() so the check works on Windows (\r\n) and Unix (\n). val out = output() - assertTrue(out.contains("\n\n") || out.endsWith("\n\n")) + val sep = System.lineSeparator() + assertTrue(out.contains("$sep$sep") || out.endsWith("$sep$sep")) } @Test diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt index 14fbe734..88243d4c 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/manager/IgContextManagerTest.kt @@ -313,9 +313,15 @@ class IgContextManagerTest { ) = emptySet() override fun read(uri: URI): InputStream? { - if (uri.toString().startsWith("file:///workspace/root1")) { + // Use contains() rather than startsWith("file:///workspace/root1") because + // Uris.parseOrNull() normalises file: URIs on Windows via File.toURI(), + // which can produce "file:////workspace/..." (4 slashes) instead of the + // expected 3-slash form. The path segment "/workspace/root1" is present + // in both forms and uniquely identifies the root. + val s = uri.toString() + if (s.contains("/workspace/root1")) { root1Reads.add(uri) - } else if (uri.toString().startsWith("file:///workspace/root2")) { + } else if (s.contains("/workspace/root2")) { root2Reads.add(uri) } return null From f981639d0cbc2bf59a709faf39dd68f8e56f8e81 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 23 Mar 2026 11:28:45 -0400 Subject: [PATCH 13/19] adds more tests --- .../server/service/CqlWorkspaceServiceTest.kt | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceServiceTest.kt diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceServiceTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceServiceTest.kt new file mode 100644 index 00000000..2bebafe8 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceServiceTest.kt @@ -0,0 +1,219 @@ +package org.opencds.cqf.cql.ls.server.service + +import org.eclipse.lsp4j.DidChangeConfigurationParams +import org.eclipse.lsp4j.DidChangeWatchedFilesParams +import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.FileChangeType +import org.eclipse.lsp4j.FileEvent +import org.eclipse.lsp4j.InitializeParams +import org.eclipse.lsp4j.ServerCapabilities +import org.eclipse.lsp4j.WorkspaceFolder +import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent +import org.eclipse.lsp4j.services.LanguageClient +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.mockito.Mockito +import org.opencds.cqf.cql.ls.server.event.DidChangeWatchedFilesEvent +import org.opencds.cqf.cql.ls.server.plugin.CommandContribution +import java.util.concurrent.CompletableFuture + +class CqlWorkspaceServiceTest { + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + /** Minimal CommandContribution whose executeCommand() returns "_result". */ + private fun fakeContribution(vararg commands: String): CommandContribution = + object : CommandContribution { + override fun getCommands() = setOf(*commands) + + override fun executeCommand(params: ExecuteCommandParams) = CompletableFuture.completedFuture("${params.command}_result") + } + + private fun buildService( + contributions: List = emptyList(), + client: LanguageClient = Mockito.mock(LanguageClient::class.java), + folders: MutableList = mutableListOf(), + eventBus: EventBus = EventBus.builder().build(), + ): CqlWorkspaceService = + CqlWorkspaceService( + CompletableFuture.completedFuture(client), + CompletableFuture.completedFuture(contributions), + folders, + eventBus, + ) + + // ------------------------------------------------------------------------- + // initialize() + // ------------------------------------------------------------------------- + + @Test + fun `initialize enables workspace folder change notifications`() { + val caps = ServerCapabilities() + buildService().initialize(InitializeParams(), caps) + // WorkspaceFoldersOptions.changeNotifications is Either; left = boolean flag + assertTrue(caps.workspace.workspaceFolders.changeNotifications.left) + } + + @Test + fun `initialize registers contributed commands in executeCommandProvider`() { + val caps = ServerCapabilities() + buildService(listOf(fakeContribution("cmd.a", "cmd.b"))).initialize(InitializeParams(), caps) + assertTrue(caps.executeCommandProvider.commands.containsAll(listOf("cmd.a", "cmd.b"))) + } + + @Test + fun `initialize adds workspace folders from InitializeParams`() { + val folders = mutableListOf() + val params = InitializeParams() + params.workspaceFolders = listOf(WorkspaceFolder("file:///ws", "ws")) + buildService(folders = folders).initialize(params, ServerCapabilities()) + assertEquals(1, folders.size) + assertEquals("file:///ws", folders[0].uri) + } + + // ------------------------------------------------------------------------- + // initialized() + // ------------------------------------------------------------------------- + + @Test + fun `initialized calls unregisterCapability then registerCapability on the client`() { + val mockClient = Mockito.mock(LanguageClient::class.java) + buildService(client = mockClient).initialized() + Mockito.verify(mockClient).unregisterCapability(Mockito.any()) + Mockito.verify(mockClient).registerCapability(Mockito.any()) + } + + // ------------------------------------------------------------------------- + // getSupportedCommands() + // ------------------------------------------------------------------------- + + @Test + fun `getSupportedCommands returns the union of all contributed commands`() { + val svc = buildService(listOf(fakeContribution("cmd.a"), fakeContribution("cmd.b"))) + val commands = svc.getSupportedCommands() + assertTrue(commands.containsAll(listOf("cmd.a", "cmd.b"))) + } + + @Test + fun `getSupportedCommands returns empty list when there are no contributions`() { + assertTrue(buildService().getSupportedCommands().isEmpty()) + } + + @Test + fun `getSupportedCommands throws IllegalArgumentException on duplicate command`() { + val svc = buildService(listOf(fakeContribution("cmd.dup"), fakeContribution("cmd.dup"))) + assertThrows { svc.getSupportedCommands() } + } + + // ------------------------------------------------------------------------- + // didChangeWorkspaceFolders() + // ------------------------------------------------------------------------- + + @Test + fun `didChangeWorkspaceFolders adds new folders`() { + val folders = mutableListOf() + val svc = buildService(folders = folders) + val added = WorkspaceFolder("file:///new", "new") + val changeEvent = WorkspaceFoldersChangeEvent() + changeEvent.added = listOf(added) + changeEvent.removed = emptyList() + val params = DidChangeWorkspaceFoldersParams() + params.event = changeEvent + svc.didChangeWorkspaceFolders(params) + assertTrue(folders.contains(added)) + } + + @Test + fun `didChangeWorkspaceFolders removes existing folders`() { + val existing = WorkspaceFolder("file:///old", "old") + val folders = mutableListOf(existing) + val svc = buildService(folders = folders) + val changeEvent = WorkspaceFoldersChangeEvent() + changeEvent.added = emptyList() + changeEvent.removed = listOf(existing) + val params = DidChangeWorkspaceFoldersParams() + params.event = changeEvent + svc.didChangeWorkspaceFolders(params) + assertFalse(folders.contains(existing)) + } + + // ------------------------------------------------------------------------- + // didChangeConfiguration() + // ------------------------------------------------------------------------- + + @Test + fun `didChangeConfiguration is a no-op and does not throw`() { + assertDoesNotThrow { buildService().didChangeConfiguration(DidChangeConfigurationParams()) } + } + + // ------------------------------------------------------------------------- + // didChangeWatchedFiles() + // ------------------------------------------------------------------------- + + @Test + fun `didChangeWatchedFiles posts DidChangeWatchedFilesEvent to the event bus`() { + val bus = EventBus.builder().build() + var received: DidChangeWatchedFilesEvent? = null + val subscriber = + object { + @Subscribe + fun on(e: DidChangeWatchedFilesEvent) { + received = e + } + } + bus.register(subscriber) + val svc = buildService(eventBus = bus) + val params = + DidChangeWatchedFilesParams( + listOf(FileEvent("file:///a.cql", FileChangeType.Changed)), + ) + svc.didChangeWatchedFiles(params) + assertNotNull(received) + assertEquals("file:///a.cql", received!!.params().changes[0].uri) + } + + // ------------------------------------------------------------------------- + // executeCommand() / executeCommandFromContributions() + // ------------------------------------------------------------------------- + + @Test + fun `executeCommand dispatches to the matching contribution`() { + val svc = buildService(listOf(fakeContribution("cmd.a"))) + val result = svc.executeCommand(ExecuteCommandParams("cmd.a", emptyList())).join() + assertEquals("cmd.a_result", result) + } + + @Test + fun `executeCommand for unknown command shows error on client and returns null`() { + val mockClient = Mockito.mock(LanguageClient::class.java) + val svc = buildService(client = mockClient) + val result = svc.executeCommand(ExecuteCommandParams("cmd.unknown", emptyList())).join() + assertNull(result) + Mockito.verify(mockClient).showMessage(Mockito.any()) + } + + @Test + fun `executeCommand wraps synchronous contribution exception into failed future`() { + val mockClient = Mockito.mock(LanguageClient::class.java) + val failContrib = + object : CommandContribution { + override fun getCommands() = setOf("cmd.fail") + + override fun executeCommand(params: ExecuteCommandParams): CompletableFuture = throw RuntimeException("boom") + } + val svc = buildService(listOf(failContrib), client = mockClient) + val result = svc.executeCommand(ExecuteCommandParams("cmd.fail", emptyList())) + assertTrue(result.isCompletedExceptionally) + Mockito.verify(mockClient).showMessage(Mockito.any()) + } +} From cfdff11e2fdf7e081899e15230c528263dc664e2 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 23 Mar 2026 11:46:41 -0400 Subject: [PATCH 14/19] fixes test issues --- .../cqf/cql/ls/server/service/CqlWorkspaceServiceTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceServiceTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceServiceTest.kt index 2bebafe8..98b924b0 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceServiceTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceServiceTest.kt @@ -60,8 +60,9 @@ class CqlWorkspaceServiceTest { fun `initialize enables workspace folder change notifications`() { val caps = ServerCapabilities() buildService().initialize(InitializeParams(), caps) - // WorkspaceFoldersOptions.changeNotifications is Either; left = boolean flag - assertTrue(caps.workspace.workspaceFolders.changeNotifications.left) + // WorkspaceFoldersOptions.changeNotifications is Either; boolean is on the right. + // Use assertEquals to avoid the assertTrue(Boolean?) nullable ambiguity in Kotlin. + assertEquals(true, caps.workspace.workspaceFolders.changeNotifications.right) } @Test From b7dae21bc711aed1ea00bac3e4cf02b45fa75b54 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 23 Mar 2026 13:41:10 -0400 Subject: [PATCH 15/19] more test cases --- .../cqf/cql/ls/server/LanguageServerTest.kt | 127 ++++++++++++++ .../server/service/DiagnosticsServiceTest.kt | 129 ++++++++++++++ .../cql/ls/server/command/CqlCommandTest.kt | 37 ++++ .../service/CqlTextDocumentServiceTest.kt | 158 ++++++++++++++++++ 4 files changed, 451 insertions(+) create mode 100644 ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentServiceTest.kt diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/LanguageServerTest.kt b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/LanguageServerTest.kt index af690657..462938a5 100644 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/LanguageServerTest.kt +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/LanguageServerTest.kt @@ -1,16 +1,24 @@ package org.opencds.cqf.cql.ls.server import org.eclipse.lsp4j.HoverParams +import org.eclipse.lsp4j.InitializeParams +import org.eclipse.lsp4j.InitializedParams import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.SetTraceParams import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.TraceValue import org.eclipse.lsp4j.services.LanguageClient import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Logger.JavaLogger import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.mockito.Mockito import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager import org.opencds.cqf.cql.ls.server.manager.IgContextManager @@ -42,6 +50,20 @@ class LanguageServerTest { CqlTextDocumentService(languageClientFuture, HoverProvider(compilationManager), FormattingProvider(cs), eventBus) ) } + + /** Builds a fresh, isolated server instance for tests that mutate server state. */ + private fun buildServer(): CqlLanguageServer { + val eventBus = EventBus.builder().build() + val cs = TestContentService() + val compilationManager = CqlCompilationManager(cs, CompilerOptionsManager(cs), IgContextManager(cs)) + val clientFuture = CompletableFuture() + val commandsFuture = CompletableFuture.completedFuture>(emptyList()) + return CqlLanguageServer( + clientFuture, + CqlWorkspaceService(clientFuture, commandsFuture, mutableListOf(), eventBus), + CqlTextDocumentService(clientFuture, HoverProvider(compilationManager), FormattingProvider(cs), eventBus) + ) + } } @Test @@ -85,4 +107,109 @@ class LanguageServerTest { assertEquals("markdown", markup.kind) assertEquals("```cql\nlist\n```", markup.value) } + + // ------------------------------------------------------------------------- + // initialize() + // ------------------------------------------------------------------------- + + @Test + fun initialize_returnsNonNullResult() { + val result = server.initialize(InitializeParams()).get() + assertNotNull(result) + } + + @Test + fun initialize_resultHasNonNullCapabilities() { + val result = server.initialize(InitializeParams()).get() + assertNotNull(result.capabilities) + } + + @Test + fun initialize_setsHoverProviderCapability() { + val result = server.initialize(InitializeParams()).get() + // CqlTextDocumentService.initialize() calls setHoverProvider(true) + assertNotNull(result.capabilities.hoverProvider) + } + + @Test + fun initialize_setsDocumentFormattingCapability() { + val result = server.initialize(InitializeParams()).get() + // CqlTextDocumentService.initialize() calls setDocumentFormattingProvider(true) + assertNotNull(result.capabilities.documentFormattingProvider) + } + + @Test + fun initialize_setsTextDocumentSyncCapability() { + val result = server.initialize(InitializeParams()).get() + // CqlTextDocumentService.initialize() calls setTextDocumentSync(TextDocumentSyncKind.Full) + assertNotNull(result.capabilities.textDocumentSync) + } + + @Test + fun initialize_setsWorkspaceFolderCapabilities() { + val result = server.initialize(InitializeParams()).get() + // CqlWorkspaceService.initialize() sets workspace folder options + assertNotNull(result.capabilities.workspace) + assertNotNull(result.capabilities.workspace.workspaceFolders) + } + + // ------------------------------------------------------------------------- + // initialized() / setTrace() + // ------------------------------------------------------------------------- + + @Test + fun initialized_doesNotThrow() { + assertDoesNotThrow { server.initialized(InitializedParams()) } + } + + @Test + fun setTrace_doesNotThrow() { + // LSP4J's default implementation throws UnsupportedOperationException; + // CqlLanguageServer overrides it as a no-op. + assertDoesNotThrow { server.setTrace(SetTraceParams(TraceValue.Verbose)) } + } + + // ------------------------------------------------------------------------- + // shutdown() + // ------------------------------------------------------------------------- + + @Test + fun shutdown_returnsDoneFuture() { + val future = server.shutdown() + assertTrue(future.isDone) + } + + @Test + fun shutdown_resultIsNull() { + assertNull(server.shutdown().get()) + } + + // ------------------------------------------------------------------------- + // exit() / exited() + // ------------------------------------------------------------------------- + + @Test + fun exit_completesExitedFutureAndWasNotDoneBefore() { + // Use a fresh server so exit() doesn't affect other tests via the shared instance. + val freshServer = buildServer() + assertFalse(freshServer.exited().isDone, "exited() should not be done before exit() is called") + freshServer.exit() + assertTrue(freshServer.exited().isDone, "exited() should be done after exit() is called") + } + + // ------------------------------------------------------------------------- + // connect() + // ------------------------------------------------------------------------- + + @Test + fun connect_completesClientFuture() { + // Use a fresh server with an incomplete client future so connect() can complete it. + val freshServer = buildServer() + val mockClient = Mockito.mock(LanguageClient::class.java) + freshServer.connect(mockClient) + // getTextDocumentService() / getWorkspaceService() are synchronous — just verify + // that the server's services are retrievable (indirectly exercises the client future). + assertNotNull(freshServer.getTextDocumentService()) + assertNotNull(freshServer.getWorkspaceService()) + } } diff --git a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.kt b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.kt index 7adae6e8..3d122da5 100644 --- a/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.kt +++ b/ls/server/src/test/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsServiceTest.kt @@ -2,22 +2,34 @@ package org.opencds.cqf.cql.ls.server.service import org.eclipse.lsp4j.Diagnostic import org.eclipse.lsp4j.DiagnosticSeverity +import org.eclipse.lsp4j.DidCloseTextDocumentParams +import org.eclipse.lsp4j.DidOpenTextDocumentParams import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.PublishDiagnosticsParams import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.TextDocumentItem import org.eclipse.lsp4j.services.LanguageClient import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test +import org.mockito.ArgumentCaptor import org.mockito.Mockito import org.opencds.cqf.cql.ls.core.utility.Uris +import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager import org.opencds.cqf.cql.ls.server.manager.IgContextManager import java.net.URI import java.util.concurrent.CompletableFuture +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean class DiagnosticsServiceTest { @@ -35,6 +47,13 @@ class DiagnosticsServiceTest { cs ) } + + /** Builds a fresh DiagnosticsService backed by a Mockito LanguageClient. */ + private fun buildService(client: LanguageClient = Mockito.mock(LanguageClient::class.java)): DiagnosticsService { + val cs = TestContentService() + val compilationManager = CqlCompilationManager(cs, CompilerOptionsManager(cs), IgContextManager(cs)) + return DiagnosticsService(CompletableFuture.completedFuture(client), compilationManager, cs) + } } @Test @@ -105,4 +124,114 @@ class DiagnosticsServiceTest { assertNotNull(d.range.start) assertNotNull(d.range.end) } + + // ------------------------------------------------------------------------- + // lint() — compile == null path (URI not in classpath → no CQL content) + // + // TestContentService.read() calls getResourceAsStream(uri.toString()), which + // returns null for any path that is not on the classpath. A null stream causes + // CqlCompilationManager.compile() to return null, triggering the + // "Library does not contain CQL content." warning branch in lint(). + // ------------------------------------------------------------------------- + + @Test + fun lint_nonExistentUri_warningMessageIsNoCqlContent() { + val uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/NonExistent.cql")!! + val d = diagnosticsService.lint(uri)[uri]!!.iterator().next() + assertEquals("Library does not contain CQL content.", d.message) + } + + @Test + fun lint_nonExistentUri_warningRangeIsZeroZero() { + val uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/NonExistent.cql")!! + val d = diagnosticsService.lint(uri)[uri]!!.iterator().next() + assertEquals(Range(Position(0, 0), Position(0, 0)), d.range) + } + + @Test + fun lint_nonExistentUri_warningSourceIsLint() { + val uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/NonExistent.cql")!! + val d = diagnosticsService.lint(uri)[uri]!!.iterator().next() + assertEquals("lint", d.source) + } + + @Test + fun lint_nonExistentUri_warningIsWarning() { + val uri = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/NonExistent.cql")!! + val d = diagnosticsService.lint(uri)[uri]!!.iterator().next() + assertEquals(DiagnosticSeverity.Warning, d.severity) + } + + // ------------------------------------------------------------------------- + // lint() — source field: only the null-compiler path sets it explicitly. + // Diagnostics.convert() (used for real compiler exceptions) does NOT set source. + // ------------------------------------------------------------------------- + + @Test + fun lint_syntaxError_diagnosticSourceIsNull() { + // Diagnostics.convert() sets severity, range, and message but leaves source null. + // Only the "Library does not contain CQL content." warning path sets source="lint". + val uri: URI = Uris.parseOrNull("/org/opencds/cqf/cql/ls/server/SyntaxError.cql")!! + val d: Diagnostic = diagnosticsService.lint(uri)[uri]!!.iterator().next() + assertNull(d.source) + } + + // ------------------------------------------------------------------------- + // Event handlers — didClose, didOpen + // ------------------------------------------------------------------------- + + @Test + fun didClose_publishesEmptyDiagnosticsForTheClosedUri() { + val mockClient = Mockito.mock(LanguageClient::class.java) + val svc = buildService(mockClient) + + val closeParams = DidCloseTextDocumentParams() + closeParams.textDocument = TextDocumentIdentifier("file:///workspace/One.cql") + svc.didClose(DidCloseTextDocumentEvent(closeParams)) + + val captor = ArgumentCaptor.forClass(PublishDiagnosticsParams::class.java) + Mockito.verify(mockClient).publishDiagnostics(captor.capture()) + assertEquals("file:///workspace/One.cql", captor.value.uri) + assertTrue(captor.value.diagnostics.isEmpty()) + } + + @Test + fun didOpen_publishesDiagnosticsToClient() { + val mockClient = Mockito.mock(LanguageClient::class.java) + val svc = buildService(mockClient) + + val item = TextDocumentItem() + item.uri = "/org/opencds/cqf/cql/ls/server/One.cql" + val openParams = DidOpenTextDocumentParams() + openParams.textDocument = item + svc.didOpen(DidOpenTextDocumentEvent(openParams)) + + // doLint() calls publishDiagnostics synchronously — verify it was called at least once. + Mockito.verify(mockClient, Mockito.atLeastOnce()).publishDiagnostics(Mockito.any()) + } + + // ------------------------------------------------------------------------- + // debounce() — task execution and cancellation + // ------------------------------------------------------------------------- + + @Test + fun debounce_taskExecutesAfterDelay() { + val svc = buildService() + val latch = CountDownLatch(1) + svc.debounce(50L) { latch.countDown() } + assertTrue(latch.await(2, TimeUnit.SECONDS), "Debounced task should execute within timeout") + } + + @Test + fun debounce_cancelsPreviousTaskWhenNewOneArrives() { + val svc = buildService() + val firstExecuted = AtomicBoolean(false) + val secondLatch = CountDownLatch(1) + + svc.debounce(500L) { firstExecuted.set(true) } // long delay — will be cancelled + svc.debounce(50L) { secondLatch.countDown() } // short delay — cancels the first + + assertTrue(secondLatch.await(2, TimeUnit.SECONDS), "Second task should execute") + assertFalse(firstExecuted.get(), "First task should have been cancelled before it ran") + } } diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt index b86ce0cb..26f277fd 100644 --- a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/command/CqlCommandTest.kt @@ -178,6 +178,43 @@ class CqlCommandTest { assertThrows { cmd.call() } } + // ------------------------------------------------------------------------- + // Tests 16–17: additional execution paths + // ------------------------------------------------------------------------- + + @Test + @Order(16) + fun `expression filter with multiple expressions includes all specified`() { + // Both "Two" and "Two List" are declared in Two.cql; requesting both explicitly + // should keep both — the filter is an inclusion list, not an exclusion list. + val cmd = buildCommand("Two", expressions = arrayOf("Two", "Two List")) + cmd.call() + val out = output() + assertTrue(out.contains("Two=2"), "Expected 'Two=2' in filtered output") + assertTrue(out.contains("Two List=[1, 2, 3]"), "Expected 'Two List' in filtered output") + } + + @Test + @Order(17) + fun `multiple libraries - each is evaluated in the same call`() { + // cmd.libraries holds two LibraryParameters; the for-loop should evaluate both + // and print results for each, separated by a blank line. + val cmd = CqlCommand() + cmd.fhirVersion = "R4" + val dir = cqlFixtureDirectoryUrl() + val lib1 = CqlCommand.LibraryParameter() + lib1.libraryUrl = dir + lib1.libraryName = "One" + val lib2 = CqlCommand.LibraryParameter() + lib2.libraryUrl = dir + lib2.libraryName = "Two" + cmd.libraries = mutableListOf(lib1, lib2) + assertEquals(0, cmd.call()) + val out = output() + assertTrue(out.contains("One=1"), "Expected 'One=1' from first library") + assertTrue(out.contains("Two=2"), "Expected 'Two=2' from second library") + } + // ------------------------------------------------------------------------- // Tests 13–15: cross-platform path handling via Uris.parseOrNull() // No CQL engine involvement — pure unit tests of URI parsing behaviour. diff --git a/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentServiceTest.kt b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentServiceTest.kt new file mode 100644 index 00000000..68261985 --- /dev/null +++ b/ls/server/src/test/kotlin/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentServiceTest.kt @@ -0,0 +1,158 @@ +package org.opencds.cqf.cql.ls.server.service + +import org.eclipse.lsp4j.DidChangeTextDocumentParams +import org.eclipse.lsp4j.DidCloseTextDocumentParams +import org.eclipse.lsp4j.DidOpenTextDocumentParams +import org.eclipse.lsp4j.DidSaveTextDocumentParams +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.TextDocumentItem +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier +import org.eclipse.lsp4j.services.LanguageClient +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.opencds.cqf.cql.ls.server.event.DidChangeTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidCloseTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidOpenTextDocumentEvent +import org.opencds.cqf.cql.ls.server.event.DidSaveTextDocumentEvent +import org.opencds.cqf.cql.ls.server.manager.CompilerOptionsManager +import org.opencds.cqf.cql.ls.server.manager.CqlCompilationManager +import org.opencds.cqf.cql.ls.server.manager.IgContextManager +import org.opencds.cqf.cql.ls.server.provider.FormattingProvider +import org.opencds.cqf.cql.ls.server.provider.HoverProvider +import java.util.concurrent.CompletableFuture + +class CqlTextDocumentServiceTest { + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private fun buildService(bus: EventBus): CqlTextDocumentService { + val cs = TestContentService() + val compilationManager = CqlCompilationManager(cs, CompilerOptionsManager(cs), IgContextManager(cs)) + return CqlTextDocumentService( + CompletableFuture(), + HoverProvider(compilationManager), + FormattingProvider(cs), + bus, + ) + } + + /** Registers [subscriber] on [bus], calls [action], then unregisters. */ + private fun withSubscriber( + bus: EventBus, + subscriber: Any, + action: () -> Unit, + ) { + bus.register(subscriber) + try { + action() + } finally { + bus.unregister(subscriber) + } + } + + // ------------------------------------------------------------------------- + // didOpen + // ------------------------------------------------------------------------- + + @Test + fun `didOpen posts DidOpenTextDocumentEvent with the correct URI`() { + val bus = EventBus.builder().build() + val svc = buildService(bus) + var received: DidOpenTextDocumentEvent? = null + val subscriber = + object { + @Subscribe fun on(e: DidOpenTextDocumentEvent) { + received = e + } + } + + val item = TextDocumentItem() + item.uri = "file:///workspace/One.cql" + val params = DidOpenTextDocumentParams() + params.textDocument = item + + withSubscriber(bus, subscriber) { svc.didOpen(params) } + + assertNotNull(received) + assertEquals("file:///workspace/One.cql", received!!.params().textDocument.uri) + } + + // ------------------------------------------------------------------------- + // didChange + // ------------------------------------------------------------------------- + + @Test + fun `didChange posts DidChangeTextDocumentEvent with the correct URI`() { + val bus = EventBus.builder().build() + val svc = buildService(bus) + var received: DidChangeTextDocumentEvent? = null + val subscriber = + object { + @Subscribe fun on(e: DidChangeTextDocumentEvent) { + received = e + } + } + + val params = DidChangeTextDocumentParams() + params.textDocument = VersionedTextDocumentIdentifier("file:///workspace/One.cql", 1) + + withSubscriber(bus, subscriber) { svc.didChange(params) } + + assertNotNull(received) + assertEquals("file:///workspace/One.cql", received!!.params().textDocument.uri) + } + + // ------------------------------------------------------------------------- + // didClose + // ------------------------------------------------------------------------- + + @Test + fun `didClose posts DidCloseTextDocumentEvent with the correct URI`() { + val bus = EventBus.builder().build() + val svc = buildService(bus) + var received: DidCloseTextDocumentEvent? = null + val subscriber = + object { + @Subscribe fun on(e: DidCloseTextDocumentEvent) { + received = e + } + } + + val params = DidCloseTextDocumentParams() + params.textDocument = TextDocumentIdentifier("file:///workspace/One.cql") + + withSubscriber(bus, subscriber) { svc.didClose(params) } + + assertNotNull(received) + assertEquals("file:///workspace/One.cql", received!!.params().textDocument.uri) + } + + // ------------------------------------------------------------------------- + // didSave + // ------------------------------------------------------------------------- + + @Test + fun `didSave posts DidSaveTextDocumentEvent with the correct URI`() { + val bus = EventBus.builder().build() + val svc = buildService(bus) + var received: DidSaveTextDocumentEvent? = null + val subscriber = + object { + @Subscribe fun on(e: DidSaveTextDocumentEvent) { + received = e + } + } + + val params = DidSaveTextDocumentParams() + params.textDocument = TextDocumentIdentifier("file:///workspace/One.cql") + + withSubscriber(bus, subscriber) { svc.didSave(params) } + + assertNotNull(received) + assertEquals("file:///workspace/One.cql", received!!.params().textDocument.uri) + } +} From 7b6f70849f7584c25b258680cb54f45ded414e91 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 23 Mar 2026 15:59:53 -0400 Subject: [PATCH 16/19] Eliminate non-null assertions (!!) from production Kotlin files --- .../cqf/cql/ls/core/utility/UrisTest.kt | 92 +++++++++++++++++++ .../cqf/cql/ls/server/command/CqlCommand.kt | 43 +++++---- .../command/ViewElmCommandContribution.kt | 9 +- .../server/manager/CompilerOptionsManager.kt | 19 ++-- .../cql/ls/server/manager/IgContextManager.kt | 9 +- .../ig/standard/IgStandardRepository.kt | 10 +- .../ls/server/service/DiagnosticsService.kt | 2 +- .../cqf/cql/ls/server/utility/Diagnostics.kt | 8 +- 8 files changed, 148 insertions(+), 44 deletions(-) diff --git a/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt index 75313e15..f58d1272 100644 --- a/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt +++ b/core/src/test/java/org/opencds/cqf/cql/ls/core/utility/UrisTest.kt @@ -2,6 +2,8 @@ package org.opencds.cqf.cql.ls.core.utility import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.EnabledOnOs import org.junit.jupiter.api.condition.OS @@ -175,4 +177,94 @@ class UrisTest { client = Uris.toClientUri(initial) assertEquals("file:/d:/src", client) } + + // ----------------------------------------------------------------------- + // addPath("/ig.ini") — documents IgContextManager fix (line 79). + // Before: Uris.addPath(parent, "/ig.ini")!! → NPE on null + // After: Uris.addPath(parent, "/ig.ini") ?: continue → graceful skip + // + // Platform | input URI | expected result + // -----------|----------------------------------------|------------------------------- + // all | http://localhost:8080/workspace/myig | .../myig/ig.ini (non-null) + // macOS/Linux| file:///home/user/workspace/myig | .../myig/ig.ini (non-null) + // Windows | file:///C:/Users/user/workspace/myig | .../myig/ig.ini (non-null) + // ----------------------------------------------------------------------- + + @Test + fun addPathIgIni_http() { + val parent = Uris.parseOrNull("http://localhost:8080/workspace/myig")!! + val result = Uris.addPath(parent, "/ig.ini") + assertNotNull(result) + assertEquals("http://localhost:8080/workspace/myig/ig.ini", result.toString()) + } + + @Test + @EnabledOnOs(OS.MAC, OS.LINUX) + fun addPathIgIni_unix() { + val parent = Uris.parseOrNull("file:///home/user/workspace/myig")!! + val result = Uris.addPath(parent, "/ig.ini") + assertNotNull(result) + assertEquals("file:///home/user/workspace/myig/ig.ini", result.toString()) + } + + @Test + @EnabledOnOs(OS.WINDOWS) + fun addPathIgIni_windows() { + val parent = Uris.parseOrNull("file:///C:/Users/user/workspace/myig")!! + val result = Uris.addPath(parent, "/ig.ini") + assertNotNull(result) + // URI remains forward-slash on Windows + assertEquals("file:///C:/Users/user/workspace/myig/ig.ini", result.toString()) + } + + // ----------------------------------------------------------------------- + // addPath chain — documents CqlCommand fix (lines 157–159). + // parseOrNull(rd) → addPath("input") → addPath("cql") + // Before: each step forced with !! → NPE on null + // After: ?.let chain → null propagates safely + // + // Platform | input | expected + // -----------|------------------------------------------|---------------------------- + // macOS/Linux| file:///home/user/projects/myproject | .../myproject/input/cql + // Windows | file:///C:/Users/user/projects/myproject | .../myproject/input/cql + // all | C:\Users\user\projects\myproject | null (parseOrNull returns null) + // ----------------------------------------------------------------------- + + @Test + @EnabledOnOs(OS.MAC, OS.LINUX) + fun cqlCommandPathChain_unix() { + val rootUri = Uris.parseOrNull("file:///home/user/projects/myproject") + assertNotNull(rootUri) + val inputUri = rootUri?.let { Uris.addPath(it, "input") } + assertNotNull(inputUri) + assertEquals("file:///home/user/projects/myproject/input", inputUri.toString()) + val cqlUri = inputUri?.let { Uris.addPath(it, "cql") } + assertNotNull(cqlUri) + assertEquals("file:///home/user/projects/myproject/input/cql", cqlUri.toString()) + } + + @Test + @EnabledOnOs(OS.WINDOWS) + fun cqlCommandPathChain_windowsForwardSlash() { + // Windows file URI with forward slashes — chain resolves successfully + val rootUri = Uris.parseOrNull("file:///C:/Users/user/projects/myproject") + assertNotNull(rootUri) + val cqlUri = rootUri + ?.let { Uris.addPath(it, "input") } + ?.let { Uris.addPath(it, "cql") } + assertNotNull(cqlUri) + } + + @Test + fun cqlCommandPathChain_windowsBackslash() { + // Raw Windows backslash path: parseOrNull returns null on all platforms. + // This documents why the ?.let chain in CqlCommand is safe — it stops here + // rather than throwing NPE. + val rootUri = Uris.parseOrNull("C:\\Users\\user\\projects\\myproject") + assertNull(rootUri, "Raw Windows backslash paths return null from parseOrNull") + val cqlUri = rootUri + ?.let { Uris.addPath(it, "input") } + ?.let { Uris.addPath(it, "cql") } + assertNull(cqlUri) + } } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt index ec10960e..d444719b 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt @@ -40,6 +40,11 @@ import java.util.concurrent.Callable @Command(name = "cql", mixinStandardHelpOptions = true) class CqlCommand : Callable { + companion object { + private val log = LoggerFactory.getLogger(CqlCommand::class.java) + } + + @Option(names = ["-fv", "--fhir-version"], required = true) var fhirVersion: String = "" @@ -154,9 +159,14 @@ class CqlCommand : Callable { igContext = IGContext(Logger()) igContext.initializeFromIg(rootDir, igPath, toVersionNumber(fhirVersionEnum)) } else if (parentCommand != null && rootDir != null) { - npmProcessor = parentCommand!! - .getIgContextManager() - .getContext(Uris.addPath(Uris.addPath(Uris.parseOrNull(rootDir!!)!!, "input")!!, "cql")!!) + val pc = parentCommand + val rd = rootDir + if (pc != null && rd != null) { + val rootUri = Uris.parseOrNull(rd) + val inputUri = rootUri?.let { Uris.addPath(it, "input") } + val cqlUri = inputUri?.let { Uris.addPath(it, "cql") } + npmProcessor = cqlUri?.let { pc.getIgContextManager().getContext(it) } + } if (npmProcessor != null) { igContext = npmProcessor.igContext } @@ -170,7 +180,9 @@ class CqlCommand : Callable { val optionsPathVal = optionsPath if (optionsPathVal != null) { - val op = Path(Paths.get(Uris.parseOrNull(optionsPathVal)!!).toString()) + val optUri = Uris.parseOrNull(optionsPathVal) + ?: run { log.warn("Could not parse options path: $optionsPathVal"); return 1 } + val op = Path(Paths.get(optUri).toString()) val options = CqlTranslatorOptions.fromFile(Path(op)) cqlOptions.setCqlCompilerOptions(options.cqlCompilerOptions) } @@ -205,28 +217,27 @@ class CqlCommand : Callable { val libraryKotlinPath = if (libraryUri != null) Path(Paths.get(libraryUri).toString()) else null - val modelPath = library.model?.modelUrl?.let { Paths.get(Uris.parseOrNull(it)!!) } + val modelPath = library.model?.modelUrl?.let { Uris.parseOrNull(it)?.let { u -> Paths.get(u) } } val terminologyUrl = library.terminologyUrl - val terminologyPath = if (terminologyUrl != null) Paths.get(Uris.parseOrNull(terminologyUrl)!!) else null + val terminologyPath = terminologyUrl?.let { Uris.parseOrNull(it)?.let { u -> Paths.get(u) } } val repository = createRepository(fhirContext, terminologyPath, modelPath) val engine = Engines.forRepository(repository, evaluationSettings) - if (library.libraryUrl != null) { - val provider = DefaultLibrarySourceProvider(libraryKotlinPath!!) + val kPath = libraryKotlinPath + if (library.libraryUrl != null && kPath != null) { + val provider = DefaultLibrarySourceProvider(kPath) engine.environment - .libraryManager!! - .librarySourceLoader - .registerProvider(provider) + .libraryManager?.librarySourceLoader + ?.registerProvider(provider) - val modelProvider = DefaultModelInfoProvider(libraryKotlinPath!!) + val modelProvider = DefaultModelInfoProvider(kPath) engine.environment - .libraryManager!! - .modelManager - .modelInfoLoader - .registerModelInfoProvider(modelProvider) + .libraryManager?.modelManager + ?.modelInfoLoader + ?.registerModelInfoProvider(modelProvider) } val identifier = VersionedIdentifier().withId(library.libraryName) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt index 40afacd0..b0e613a5 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt @@ -47,14 +47,17 @@ class ViewElmCommandContribution(private val cqlCompilationManager: CqlCompilati return try { val uri = Uris.parseOrNull(uriString) - val compiler = cqlCompilationManager.compile(uri!!) + ?: return CompletableFuture.completedFuture(null) + val compiler = cqlCompilationManager.compile(uri) if (compiler != null) { + val library = compiler.library + ?: return CompletableFuture.completedFuture(null) // Use equalsIgnoreCase for better robustness if (elmType.equals("xml", ignoreCase = true)) { - CompletableFuture.completedFuture(convertToXml(compiler.library!!)) + CompletableFuture.completedFuture(convertToXml(library)) } else { - CompletableFuture.completedFuture(convertToJson(compiler.library!!)) + CompletableFuture.completedFuture(convertToJson(library)) } } else { CompletableFuture.completedFuture(null) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt index b7705a30..cbf2d73c 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt @@ -32,30 +32,25 @@ class CompilerOptionsManager(private val contentService: ContentService) { } protected fun readOptions(rootUri: URI): CqlCompilerOptions { - var options: CqlCompilerOptions? - val optionsUri = Uris.addPath(rootUri, "/cql/cql-options.json") - val input = contentService.read(optionsUri!!) + val input = optionsUri?.let { contentService.read(it) } input?.close() - options = if (input != null) { + val options = if (input != null && optionsUri != null) { try { CqlTranslatorOptions.fromFile(Path(NioPaths.get(optionsUri).toString())) .cqlCompilerOptions + ?: CqlTranslatorOptions.defaultOptions().cqlCompilerOptions } catch (e: Exception) { log.info("Exception ${e.message} attempting to load options from $optionsUri, using default options") - null + CqlTranslatorOptions.defaultOptions().cqlCompilerOptions } } else { log.info("$optionsUri not found, using default options") - null - } - - if (options == null) { - options = CqlTranslatorOptions.defaultOptions().cqlCompilerOptions + CqlTranslatorOptions.defaultOptions().cqlCompilerOptions } - return options!!.withOptions( + return requireNotNull(options) { "CqlCompilerOptions must not be null" }.withOptions( CqlCompilerOptions.Options.EnableLocators, CqlCompilerOptions.Options.EnableResultTypes, CqlCompilerOptions.Options.EnableAnnotations @@ -66,7 +61,7 @@ class CompilerOptionsManager(private val contentService: ContentService) { fun onMessageEvent(event: DidChangeWatchedFilesEvent) { for (e in event.params().changes) { if (e.uri.endsWith("cql-options.json")) { - clearOptions(Uris.parseOrNull(e.uri)!!) + Uris.parseOrNull(e.uri)?.let { clearOptions(it) } } } } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt index 10df493f..295ef543 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/IgContextManager.kt @@ -46,9 +46,8 @@ class IgContextManager(private val contentService: ContentService) { val npmProcessor = getContext(uri) ?: return val namespaceManager = libraryManager.namespaceManager npmProcessor.igNamespace?.let { namespaceManager.ensureNamespaceRegistered(it) } - val reader: ILibraryReader = org.cqframework.fhir.npm.LibraryLoader( - npmProcessor.igContext!!.fhirVersion - ) + val fhirVersion = npmProcessor.igContext?.fhirVersion ?: return + val reader: ILibraryReader = org.cqframework.fhir.npm.LibraryLoader(fhirVersion) val adapter = LoggerAdapter(log) val npmList = npmProcessor.getPackageManager().npmList libraryManager.librarySourceLoader.registerProvider( @@ -76,7 +75,7 @@ class IgContextManager(private val contentService: ContentService) { val parent = Uris.getHead(current) if (parent != current) { current = parent - val igIniPath = Uris.addPath(parent, "/ig.ini")!! + val igIniPath = Uris.addPath(parent, "/ig.ini") ?: continue log.info("Attempting to read ini from path {}", igIniPath) val input = contentService.read(igIniPath) if (input != null) { @@ -95,7 +94,7 @@ class IgContextManager(private val contentService: ContentService) { fun onMessageEvent(event: DidChangeWatchedFilesEvent) { for (e in event.params().changes) { if (e.uri.endsWith("ig.ini")) { - clearContext(Uris.parseOrNull(e.uri)!!) + Uris.parseOrNull(e.uri)?.let { clearContext(it) } } } } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt index 281c6b7d..aa647375 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt @@ -154,7 +154,9 @@ open class IgStandardRepository : IRepository { } val category = IgStandardResourceCategory.forType(resourceType.simpleName) - val directory = CATEGORY_DIRECTORIES[category]!! + val directory = requireNotNull(CATEGORY_DIRECTORIES[category]) { + "No directory configured for category: $category" + } val categoryPath = root.resolve(directory) if (conventions.compartmentLayout == IgStandardConventions.CompartmentLayout.DIRECTORY_PER_COMPARTMENT && @@ -227,9 +229,10 @@ open class IgStandardRepository : IRepository { protected open fun writeResource(resource: T, path: Path) { try { + val encoding = encodingForPath(path) ?: return path.parent?.toFile()?.mkdirs() FileOutputStream(path.toFile()).use { stream -> - val result = parserForEncoding(fhirContext, encodingForPath(path)!!) + val result = parserForEncoding(fhirContext, encoding) .setPrettyPrint(true) .encodeResourceToString(resource) stream.write(result.toByteArray()) @@ -280,8 +283,9 @@ open class IgStandardRepository : IRepository { .parallel() .map { cachedReadResource(it) } .filter { it != null } + .map { checkNotNull(it) } .forEach { r -> - if (r!!.fhirType() != resourceClass.simpleName) return@forEach + if (r.fhirType() != resourceClass.simpleName) return@forEach val validated = validateResource(resourceClass, r, r.idElement) resources[r.idElement.toUnqualifiedVersionless()] = validated } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt index 211d8f97..28d75816 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt @@ -170,7 +170,7 @@ class DiagnosticsService( } internal fun debounce(delay: Long, task: Runnable) { - if (future != null && !future!!.isDone) future!!.cancel(false) + future?.takeIf { !it.isDone }?.cancel(false) future = executor.schedule(task, delay, TimeUnit.MILLISECONDS) } } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt index f0ae1df9..08b2ac71 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt @@ -1,6 +1,7 @@ package org.opencds.cqf.cql.ls.server.utility import org.cqframework.cql.cql2elm.CqlCompilerException +import org.cqframework.cql.cql2elm.tracking.TrackBack import org.eclipse.lsp4j.Diagnostic import org.eclipse.lsp4j.DiagnosticSeverity import org.eclipse.lsp4j.Position @@ -9,8 +10,8 @@ import org.eclipse.lsp4j.Range object Diagnostics { @JvmStatic fun convert(error: CqlCompilerException): Diagnostic? { - if (error.locator == null) return null - val range = position(error) + val locator = error.locator ?: return null + val range = position(locator) val diagnostic = Diagnostic() diagnostic.severity = severity(error.severity) diagnostic.range = range @@ -36,8 +37,7 @@ object Diagnostics { } } - private fun position(error: CqlCompilerException): Range { - val locator = error.locator!! + private fun position(locator: TrackBack): Range { return Range( Position( maxOf(locator.startLine - 1, 0), From fafc62eb8d739cc54517074debd56e341404cbe9 Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 23 Mar 2026 16:13:28 -0400 Subject: [PATCH 17/19] Fix unsafe casts from Kotlin migration code review --- .../org/opencds/cqf/cql/ls/server/command/CqlCommand.kt | 6 +++--- .../cql/ls/server/command/DebugCqlCommandContribution.kt | 2 +- .../cql/ls/server/command/ViewElmCommandContribution.kt | 5 +++-- .../server/repository/ig/standard/IgStandardRepository.kt | 8 ++++---- .../ig/standard/IgStandardRepositoryCompartment.kt | 3 +-- .../cqf/cql/ls/server/service/CqlTextDocumentService.kt | 7 ++++--- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt index d444719b..0e5ab6fc 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/CqlCommand.kt @@ -243,9 +243,9 @@ class CqlCommand : Callable { val identifier = VersionedIdentifier().withId(library.libraryName) val contextParameter: org.apache.commons.lang3.tuple.Pair? = - if (library.context != null) { - org.apache.commons.lang3.tuple.Pair.of(library.context!!.contextName, library.context!!.contextValue as Any?) - } else null + library.context?.let { ctx -> + org.apache.commons.lang3.tuple.Pair.of(ctx.contextName, ctx.contextValue) + } val expressions = library.expression?.toSet() val result = if (expressions != null) { diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.kt index 9f47370b..9f3a3731 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/DebugCqlCommandContribution.kt @@ -32,7 +32,7 @@ class DebugCqlCommandContribution(private val igContextManager: IgContextManager private fun executeCql(params: ExecuteCommandParams): CompletableFuture { try { val arguments = params.arguments - .map { it as JsonElement } + .mapNotNull { it as? JsonElement } .map { it.asString } // Temporarily redirect std out, because uh... I didn't do that very smart. diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt index b0e613a5..bd3cfe3e 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt @@ -36,11 +36,12 @@ class ViewElmCommandContribution(private val cqlCompilationManager: CqlCompilati return CompletableFuture.completedFuture(null) } - val uriString = (args[0] as JsonElement).asString + val uriString = (args[0] as? JsonElement)?.asString + ?: return CompletableFuture.completedFuture(null) // Handle missing or null elmType by defaulting to "xml" val elmType = if (args.size > 1 && args[1] != null) { - (args[1] as JsonElement).asString + (args[1] as? JsonElement)?.asString ?: "xml" } else { "xml" } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt index aa647375..aa25e9e1 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt @@ -339,7 +339,7 @@ open class IgStandardRepository : IRepository { } private fun validateResource(resourceType: Class, resource: IBaseResource, id: IIdType): T { - val path = resource.getUserData(SOURCE_PATH_TAG) as Path? + val path = resource.getUserData(SOURCE_PATH_TAG) as? Path if (resourceType.simpleName != resource.fhirType()) { throw ResourceNotFoundException( @@ -374,7 +374,7 @@ open class IgStandardRepository : IRepository { val compartment = compartmentFrom(headers) val preferred = preferredPathForResource(resource.javaClass, resource.idElement, compartment) - var actual = resource.getUserData(SOURCE_PATH_TAG) as Path? ?: preferred + var actual = (resource.getUserData(SOURCE_PATH_TAG) as? Path) ?: preferred if (isExternalPath(actual)) { throw ForbiddenOperationException( @@ -496,9 +496,9 @@ open class IgStandardRepository : IRepository { protected open fun invokeOperation( id: IIdType?, resourceType: String, operationName: String, parameters: IBaseParameters ): R { - checkNotNull(operationProvider) { "No operation provider found. Unable to invoke operations." } + val provider = checkNotNull(operationProvider) { "No operation provider found. Unable to invoke operations." } @Suppress("UNCHECKED_CAST") - return operationProvider!!.invokeOperation, R>( + return provider.invokeOperation, R>( this, id, resourceType, operationName, parameters ) } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.kt index 1f720c89..b6f104eb 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepositoryCompartment.kt @@ -27,8 +27,7 @@ class IgStandardRepositoryCompartment { fun isEmpty(): Boolean = type == null || id == null override fun equals(other: Any?): Boolean { - if (other == null || javaClass != other.javaClass) return false - other as IgStandardRepositoryCompartment + if (other !is IgStandardRepositoryCompartment) return false return type == other.type && id == other.id } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt index 5094ade0..ba14e52d 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlTextDocumentService.kt @@ -50,10 +50,11 @@ class CqlTextDocumentService( // c.setSignatureHelpProvider(new SignatureHelpOptions(ImmutableList.of("(", ","))); } - @Suppress("UNCHECKED_CAST") override fun hover(position: HoverParams): CompletableFuture { - return CompletableFuture.supplyAsync { hoverProvider.hover(position) } - .exceptionally { notifyClient(it) } as CompletableFuture + val result = CompletableFuture.supplyAsync { hoverProvider.hover(position) } + .exceptionally { notifyClient(it) } + @Suppress("UNCHECKED_CAST") + return result as CompletableFuture } override fun formatting(params: DocumentFormattingParams): CompletableFuture> { From 08353a7228feef5ef536f5217a84e099677c319e Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 23 Mar 2026 16:25:55 -0400 Subject: [PATCH 18/19] replaces loops with map/filters --- .../ig/standard/IgStandardRepository.kt | 35 ++++++---------- .../ls/server/service/ActiveContentService.kt | 41 ++++++------------- .../ls/server/service/CqlWorkspaceService.kt | 15 +++---- .../ls/server/service/DiagnosticsService.kt | 4 -- 4 files changed, 30 insertions(+), 65 deletions(-) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt index aa25e9e1..afe283fc 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/repository/ig/standard/IgStandardRepository.kt @@ -122,22 +122,18 @@ open class IgStandardRepository : IRepository { protected open fun potentialPathsForResource( resourceType: Class, id: I, igRepositoryCompartment: IgStandardRepositoryCompartment ): List { - val potentialDirectories = mutableListOf() val directory = directoryForResource(resourceType, igRepositoryCompartment) - potentialDirectories.add(directory) - - if (IgStandardResourceCategory.forType(resourceType.simpleName) == IgStandardResourceCategory.TERMINOLOGY) { - potentialDirectories.add(directory.resolve(EXTERNAL_DIRECTORY)) + val potentialDirectories = buildList { + add(directory) + if (IgStandardResourceCategory.forType(resourceType.simpleName) == IgStandardResourceCategory.TERMINOLOGY) { + add(directory.resolve(EXTERNAL_DIRECTORY)) + } } - - val potentialPaths = mutableListOf() - for (dir in potentialDirectories) { - for (encoding in FILE_EXTENSIONS.keys) { - potentialPaths.add(dir.resolve(fileNameForResource(resourceType.simpleName, id.idPart, encoding))) + return potentialDirectories.flatMap { dir -> + FILE_EXTENSIONS.keys.map { encoding -> + dir.resolve(fileNameForResource(resourceType.simpleName, id.idPart, encoding)) } } - - return potentialPaths } protected open fun fileNameForResource(resourceType: String, resourceId: String, encoding: EncodingEnum): String { @@ -459,17 +455,12 @@ open class IgStandardRepository : IRepository { private fun getIdCandidates( idQueries: Collection>, resourceIdMap: Map, resourceType: Class ): List { - val idResources = mutableListOf() - for (idQuery in idQueries) { - for (query in idQuery) { - if (query is TokenParam) { - val id = Ids.newId(fhirContext, resourceType.simpleName, query.value) - val resource = resourceIdMap[id] - if (resource != null) idResources.add(resource) - } + return idQueries.flatten() + .filterIsInstance() + .mapNotNull { query -> + val id = Ids.newId(fhirContext, resourceType.simpleName, query.value) + resourceIdMap[id] } - } - return idResources } private fun allParametersMatch( diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt index 0005b797..5d628f98 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt @@ -89,46 +89,29 @@ class ActiveContentService : ContentService { val reader = BufferedReader(StringReader(sourceText)) val writer = StringWriter() - var line = 0 - while (line < range.start.line) { - writer.write(reader.readLine() + '\n') - line++ - } - - for (character in 0 until range.start.character) { - writer.write(reader.read()) - } + repeat(range.start.line) { writer.write(reader.readLine() + '\n') } + repeat(range.start.character) { writer.write(reader.read()) } val encodedText = String(change.text.toByteArray(StandardCharsets.UTF_8), StandardCharsets.UTF_8) writer.write(encodedText) reader.skip(change.rangeLength.toLong()) - while (true) { - val next = reader.read() - if (next == -1) return writer.toString() - else writer.write(next) - } + generateSequence { reader.read().takeIf { it != -1 } } + .forEach { writer.write(it) } + return writer.toString() } internal fun searchActiveContent(root: URI, identifier: VersionedIdentifier): Set { val id = identifier.id val version = identifier.version - var matchText = "(?s).*library\\s+$id" - matchText += if (version != null) { - "\\s+version\\s+'$version'\\s+(?s).*" - } else { - "'\\s+(?s).*" - } - - val uris = mutableSetOf() - for ((uri, content) in activeContent.entries) { - if (root.relativize(uri) == uri) continue - if (content.content.matches(matchText.toRegex())) { - uris.add(uri) - } - } - return uris + val matchText = "(?s).*library\\s+$id" + + if (version != null) "\\s+version\\s+'$version'\\s+(?s).*" else "'\\s+(?s).*" + val pattern = matchText.toRegex() + return activeContent + .filterKeys { root.relativize(it) != it } + .filterValues { it.content.matches(pattern) } + .keys.toSet() } fun activeUris(): Set = activeContent.keys diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt index f9efe75d..1430fcec 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt @@ -148,16 +148,11 @@ class CqlWorkspaceService( } fun getSupportedCommands(): List { - val commands = mutableSetOf() - for (commandContribution in commandContributions.join()) { - for (command in commandContribution.getCommands()) { - if (commands.contains(command)) { - throw IllegalArgumentException("The command $command was contributed multiple times") - } - commands.add(command) - } - } - return ImmutableList.copyOf(commands) + val allCommands = commandContributions.join().flatMap { it.getCommands() } + allCommands.groupingBy { it }.eachCount() + .entries.firstOrNull { it.value > 1 } + ?.let { (cmd, _) -> throw IllegalArgumentException("The command $cmd was contributed multiple times") } + return ImmutableList.copyOf(allCommands) } fun stop() { diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt index 28d75816..9f31bfc1 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/DiagnosticsService.kt @@ -1,7 +1,6 @@ package org.opencds.cqf.cql.ls.server.service import com.google.common.base.Joiner -import com.google.common.base.Preconditions.checkNotNull import org.cqframework.cql.cql2elm.CqlCompilerException import org.eclipse.lsp4j.Diagnostic import org.eclipse.lsp4j.DiagnosticSeverity @@ -65,9 +64,6 @@ class DiagnosticsService( currentDiagnostics: MutableMap>, newDiagnostics: Map> ) { - checkNotNull(currentDiagnostics) - checkNotNull(newDiagnostics) - for ((uri, diagnostics) in newDiagnostics) { currentDiagnostics.getOrPut(uri) { mutableSetOf() }.addAll(diagnostics) } From 83cda2cb40c83948728c09c2bd8ac1883af3105d Mon Sep 17 00:00:00 2001 From: raleigh-g-thompson Date: Mon, 23 Mar 2026 16:51:16 -0400 Subject: [PATCH 19/19] replaces code with more kotlin idiomatic approches --- .../command/ViewElmCommandContribution.kt | 25 ++++-------- .../server/manager/CompilerOptionsManager.kt | 2 +- .../ls/server/service/ActiveContentService.kt | 14 +++---- .../ls/server/service/CqlWorkspaceService.kt | 14 +++---- .../server/service/FederatedContentService.kt | 7 ++-- .../ls/server/service/FileContentService.kt | 5 +-- .../cqf/cql/ls/server/utility/Diagnostics.kt | 10 +---- .../visitor/ExpressionTrackBackVisitor.kt | 2 +- .../command/ViewElmCommandContributionTest.kt | 39 +++++++++++++++++++ 9 files changed, 67 insertions(+), 51 deletions(-) diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt index bd3cfe3e..f4708177 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/command/ViewElmCommandContribution.kt @@ -39,32 +39,21 @@ class ViewElmCommandContribution(private val cqlCompilationManager: CqlCompilati val uriString = (args[0] as? JsonElement)?.asString ?: return CompletableFuture.completedFuture(null) - // Handle missing or null elmType by defaulting to "xml" - val elmType = if (args.size > 1 && args[1] != null) { - (args[1] as? JsonElement)?.asString ?: "xml" - } else { - "xml" - } + val elmType = (args.getOrNull(1) as? JsonElement)?.asString ?: "xml" return try { val uri = Uris.parseOrNull(uriString) ?: return CompletableFuture.completedFuture(null) val compiler = cqlCompilationManager.compile(uri) - - if (compiler != null) { - val library = compiler.library - ?: return CompletableFuture.completedFuture(null) - // Use equalsIgnoreCase for better robustness - if (elmType.equals("xml", ignoreCase = true)) { - CompletableFuture.completedFuture(convertToXml(library)) - } else { - CompletableFuture.completedFuture(convertToJson(library)) - } + ?: return CompletableFuture.completedFuture(null) + val library = compiler.library + ?: return CompletableFuture.completedFuture(null) + if (elmType.equals("xml", ignoreCase = true)) { + CompletableFuture.completedFuture(convertToXml(library)) } else { - CompletableFuture.completedFuture(null) + CompletableFuture.completedFuture(convertToJson(library)) } } catch (e: Exception) { - // Log the error here if possible to avoid "silent" failures CompletableFuture.completedFuture(null) } } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt index cbf2d73c..cd6a231f 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/manager/CompilerOptionsManager.kt @@ -36,7 +36,7 @@ class CompilerOptionsManager(private val contentService: ContentService) { val input = optionsUri?.let { contentService.read(it) } input?.close() - val options = if (input != null && optionsUri != null) { + val options = if (input != null) { try { CqlTranslatorOptions.fromFile(Path(NioPaths.get(optionsUri).toString())) .cqlCompilerOptions diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt index 5d628f98..41d51ecf 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/ActiveContentService.kt @@ -1,7 +1,5 @@ package org.opencds.cqf.cql.ls.server.service -import com.google.common.base.Preconditions.checkNotNull -import com.google.common.base.Preconditions.checkState import org.eclipse.lsp4j.TextDocumentContentChangeEvent import org.eclipse.lsp4j.TextDocumentIdentifier import org.eclipse.lsp4j.TextDocumentItem @@ -29,21 +27,21 @@ class ActiveContentService : ContentService { private val activeContent = ConcurrentHashMap() override fun locate(root: URI, identifier: VersionedIdentifier): Set { - checkNotNull(root) - checkNotNull(identifier) + requireNotNull(root) + requireNotNull(identifier) return searchActiveContent(root, identifier) } override fun read(root: URI, identifier: VersionedIdentifier): InputStream? { - checkNotNull(root) - checkNotNull(identifier) + requireNotNull(root) + requireNotNull(identifier) val uris = locate(root, identifier) - checkState(uris.size == 1, "Found more than one file for identifier: %s", identifier) + check(uris.size == 1) { "Found more than one file for identifier: $identifier" } return read(uris.first()) } override fun read(uri: URI): InputStream? { - checkNotNull(uri) + requireNotNull(uri) val content = activeContent[uri]?.content ?: return null return ByteArrayInputStream(content.toByteArray()) } diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt index 1430fcec..df8ebf11 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/CqlWorkspaceService.kt @@ -136,15 +136,13 @@ class CqlWorkspaceService( protected fun executeCommandFromContributions(params: ExecuteCommandParams): CompletableFuture { val command = params.command - - for (commandContribution in commandContributions.join()) { - if (commandContribution.getCommands().contains(command)) { - return commandContribution.executeCommand(params) + return commandContributions.join() + .firstOrNull { it.getCommands().contains(command) } + ?.executeCommand(params) + ?: run { + client.join().showMessage(MessageParams(MessageType.Error, "Unknown Command $command")) + CompletableFuture.completedFuture(null) } - } - - client.join().showMessage(MessageParams(MessageType.Error, "Unknown Command $command")) - return CompletableFuture.completedFuture(null) } fun getSupportedCommands(): List { diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.kt index 7c2cc9da..aa36fef3 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FederatedContentService.kt @@ -1,6 +1,5 @@ package org.opencds.cqf.cql.ls.server.service -import com.google.common.base.Preconditions.checkNotNull import org.hl7.elm.r1.VersionedIdentifier import org.opencds.cqf.cql.ls.core.ContentService import java.io.InputStream @@ -12,15 +11,15 @@ class FederatedContentService( ) : ContentService { override fun locate(root: URI, identifier: VersionedIdentifier): Set { - checkNotNull(root) - checkNotNull(identifier) + requireNotNull(root) + requireNotNull(identifier) val locations = activeContentService.locate(root, identifier).toMutableSet() locations.addAll(fileContentService.locate(root, identifier)) return locations } override fun read(uri: URI): InputStream? { - checkNotNull(uri) + requireNotNull(uri) return if (activeContentService.activeUris().contains(uri)) { activeContentService.read(uri) } else { diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.kt index 70a5a8ef..539646ff 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/service/FileContentService.kt @@ -1,6 +1,5 @@ package org.opencds.cqf.cql.ls.server.service -import com.google.common.base.Preconditions.checkNotNull import org.apache.commons.io.FileUtils import org.apache.commons.io.filefilter.IOFileFilter import org.apache.commons.io.filefilter.TrueFileFilter @@ -97,8 +96,8 @@ class FileContentService(protected val workspaceFolders: List) } override fun locate(root: URI, identifier: VersionedIdentifier): Set { - checkNotNull(root) - checkNotNull(identifier) + requireNotNull(root) + requireNotNull(identifier) val uris = mutableSetOf() for (w in workspaceFolders) { diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt index 08b2ac71..6264080a 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/utility/Diagnostics.kt @@ -20,14 +20,8 @@ object Diagnostics { } @JvmStatic - fun convert(errors: Iterable): Set { - val result = mutableSetOf() - for (error in errors) { - val diagnostic = convert(error) - if (diagnostic != null) result.add(diagnostic) - } - return result - } + fun convert(errors: Iterable): Set = + errors.mapNotNull { convert(it) }.toSet() private fun severity(severity: CqlCompilerException.ErrorSeverity): DiagnosticSeverity { return when (severity) { diff --git a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.kt b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.kt index 0bf49c89..091c94d6 100644 --- a/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.kt +++ b/ls/server/src/main/java/org/opencds/cqf/cql/ls/server/visitor/ExpressionTrackBackVisitor.kt @@ -12,7 +12,7 @@ open class ExpressionTrackBackVisitor : BaseElmLibraryVisitor