From ad8c309c3c8cc695523bb52dadd9cf6209b0e8d1 Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Wed, 14 Oct 2026 19:41:09 +0100 Subject: [PATCH 1/3] fix: resolve the open SonarCloud issues Fixes the code smells SonarCloud reports on main that the nullness change does not already cover. - SolrNativeHints is now the RuntimeHintsRegistrar itself, imported by SolrConfig, instead of an empty @Configuration wrapping a nested Registrar. - Empty bodies say why they are empty; Dtos record components document themselves with @param instead of dangling Javadoc. - buildSrc reads SBOM licenses through LicenseChoice.getItems(), as CycloneDX 13 deprecates getLicenses() and getExpression(); a new test covers the SPDX-expression branch. - Tests: MainTest asserts the mocked context, assertThrows lambdas hold only the call under test, three DistributedTracingTest duplicates that asserted only "some span exists" are removed, and the Docker stdio test waits with Awaitility instead of Thread.sleep. - The one false positive, a '/' URL path separator flagged as a file path delimiter, is suppressed in place with the reason. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Aditya Parikh --- .../solr/mcp/build/GenerateBinaryLicense.kt | 13 +- .../solr/mcp/build/LicenseNoticeTasksTest.kt | 18 ++ dev-docs/graalvm-native-image.md | 10 +- .../java/org/apache/solr/mcp/server/Main.java | 4 +- .../server/collection/CollectionService.java | 6 +- .../solr/mcp/server/collection/Dtos.java | 146 ++++-------- .../solr/mcp/server/config/SolrConfig.java | 9 +- .../config/SolrConfigurationProperties.java | 2 +- .../mcp/server/config/SolrNativeHints.java | 213 +++++++++--------- .../documentcreator/JsonDocumentCreator.java | 16 +- .../MarkdownDocumentCreator.java | 23 +- .../solr/mcp/server/BuildInfoReader.java | 4 +- .../apache/solr/mcp/server/ClientHttp.java | 2 +- .../apache/solr/mcp/server/ClientStdio.java | 2 +- .../org/apache/solr/mcp/server/MainTest.java | 12 +- .../mcp/server/SampleDataRuntimeHints.java | 3 - .../server/config/SolrNativeHintsTest.java | 6 +- .../DockerImageStdioIntegrationTest.java | 9 +- .../observability/DistributedTracingTest.java | 59 ----- .../mcp/server/schema/SchemaServiceTest.java | 27 +-- .../search/SearchServiceIntegrationTest.java | 19 +- .../mcp/server/search/SearchServiceTest.java | 15 +- .../mcp/server/search/SortClauseTest.java | 9 +- 23 files changed, 258 insertions(+), 369 deletions(-) diff --git a/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/GenerateBinaryLicense.kt b/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/GenerateBinaryLicense.kt index 3da0c157..69c8b449 100644 --- a/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/GenerateBinaryLicense.kt +++ b/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/GenerateBinaryLicense.kt @@ -164,12 +164,15 @@ abstract class GenerateBinaryLicense : DefaultTask() { private fun licensesOf(component: Component): List { val choice = component.licenses ?: return emptyList() val out = LinkedHashMap() - for (license in choice.licenses.orEmpty()) { - val id = license.id - val label = id ?: license.name ?: "Unspecified" - out.putIfAbsent(label, license.url ?: id?.let { spdxUrl(it) }) + for (item in choice.items.orEmpty()) { + val license = item.license + if (license != null) { + val id = license.id + val label = id ?: license.name ?: "Unspecified" + out.putIfAbsent(label, license.url ?: id?.let { spdxUrl(it) }) + } + item.expression?.value?.let { out.putIfAbsent(it, null) } } - choice.expression?.value?.let { out.putIfAbsent(it, null) } return out.map { License(it.key, it.value) } } diff --git a/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/LicenseNoticeTasksTest.kt b/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/LicenseNoticeTasksTest.kt index 457ccd6d..e3f904a3 100644 --- a/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/LicenseNoticeTasksTest.kt +++ b/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/LicenseNoticeTasksTest.kt @@ -81,6 +81,24 @@ class LicenseNoticeTasksTest { assertTrue(text.contains("License: BSD licence — http://antlr.org/license.html")) } + @Test + fun `license reports an SPDX expression as written`() { + val task = licenseTask() + write("LICENSE", "BASE").let(task.baseLicense::set) + write( + "sbom.json", + """{"components":[{"group":"org.example","name":"dual","version":"1.0", + "licenses":[{"expression":"EPL-2.0 OR GPL-2.0-with-classpath-exception"}]}]}""", + ).let(task.sbom::set) + task.bundledCoordinates.set(listOf("org.example:dual:1.0")) + val out = File(tempDir, "out/LICENSE") + task.outputFile.set(out) + + task.generate() + + assertTrue(out.readText().contains("License: EPL-2.0 OR GPL-2.0-with-classpath-exception")) + } + @Test fun `license gate fails when a bundled dependency is absent from the SBOM`() { val task = licenseTask() diff --git a/dev-docs/graalvm-native-image.md b/dev-docs/graalvm-native-image.md index de0b7b37..930d052a 100644 --- a/dev-docs/graalvm-native-image.md +++ b/dev-docs/graalvm-native-image.md @@ -123,10 +123,10 @@ from the `PROFILES` env var; the native image already made it at *build time*. Spring AOT generates most hints automatically, but a few types are invisible to it and must be registered by hand. They live in -`SolrConfig`'s sibling `SolrNativeHints.java`, a `@Configuration` annotated -`@ImportRuntimeHints(...)`. Registering hints through a `RuntimeHintsRegistrar` -(rather than scattering `@RegisterReflection` annotations) keeps the whole -reflective surface in one reviewable place. The class is registered +`SolrNativeHints.java`, a `RuntimeHintsRegistrar` that `SolrConfig` imports with +`@ImportRuntimeHints(SolrNativeHints.class)`. Registering hints through one +registrar (rather than scattering `@RegisterReflection` annotations) keeps the +whole reflective surface in one reviewable place. The registrar is imported unconditionally; on the JVM path it is simply a no-op. ### Wire format choice underpins the small hint surface @@ -175,7 +175,7 @@ container and value types. 1. Reproduce with `./gradlew nativeTest -Pnative` (or run the native binary and trigger the failing path). GraalVM reports the missing reflective/resource element. -2. Add a targeted registration in `SolrNativeHints.Registrar` — a single +2. Add a targeted registration in `SolrNativeHints` — a single `registerType(...)` with `INVOKE_DECLARED_CONSTRUCTORS`, `INVOKE_DECLARED_METHODS`, `DECLARED_FIELDS`, or a `registerPattern(...)` for a resource. Prefer this over the tracing agent so the rule is explicit and diff --git a/src/main/java/org/apache/solr/mcp/server/Main.java b/src/main/java/org/apache/solr/mcp/server/Main.java index a328f3cf..8812d219 100644 --- a/src/main/java/org/apache/solr/mcp/server/Main.java +++ b/src/main/java/org/apache/solr/mcp/server/Main.java @@ -93,7 +93,9 @@ public class Main { /** Default constructor used by Spring Boot to bootstrap the application. */ - public Main() {} + public Main() { + // Declared only to carry the Javadoc above; javadoc warns on an implicit constructor. + } /** * Logback's own system property naming the status listener to install during its diff --git a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java index fa5a62f0..030b792a 100644 --- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java +++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java @@ -824,8 +824,8 @@ private boolean isHandlerStatsEmpty(HandlerStats stats) { Long errors = getLong(coreMetrics, keyPrefix + ERRORS_FIELD); Long timeouts = getLong(coreMetrics, keyPrefix + TIMEOUTS_FIELD); Long totalTime = getLong(coreMetrics, keyPrefix + TOTAL_TIME_FIELD); - // avgTimePerRequest and avgRequestsPerSecond are not available as flat metrics; - // compute avgTimePerRequest from totalTime/requests when possible + // Solr has no flat metric for the average time per request or for requests per + // second, so the average time is derived from the total time and the request count. Float avgTimePerRequest = (requests > 0 && totalTime != null) ? (float) totalTime / requests : null; return new HandlerInfo(requests, errors, timeouts, totalTime, avgTimePerRequest, null); @@ -1051,7 +1051,7 @@ public String setupCollectionPrompt( required = false) @Nullable String purpose) { String purposeLine = - (purpose == null || purpose.isBlank()) ? "" : "\nPurpose: %s\n".formatted(purpose.strip()); + (purpose == null || purpose.isBlank()) ? "" : "\nPurpose: " + purpose.strip() + "\n"; return """ You are setting up a new Solr collection named `%s` through MCP tools.%s 1. Validate the name. diff --git a/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java b/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java index 3c389089..38cd1dc8 100644 --- a/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java +++ b/src/main/java/org/apache/solr/mcp/server/collection/Dtos.java @@ -22,24 +22,6 @@ import java.time.Instant; import org.jspecify.annotations.Nullable; -/** - * Data Transfer Objects (DTOs) for the Apache Solr MCP Server. - * - *

This package contains all the data transfer objects used to serialize and deserialize Solr - * metrics, search results, and health status information for Model Context Protocol (MCP) clients. - * All DTOs use Java records for immutability and Jackson annotations for JSON serialization. - * - *

Key Features: - * - *

    - *
  • Automatic null value exclusion from JSON output using - * {@code @JsonInclude(JsonInclude.Include.NON_NULL)} - *
  • Resilient JSON parsing with {@code @JsonIgnoreProperties(ignoreUnknown = true)} - *
  • Immutable data structures using Java records - *
  • ISO 8601 timestamp formatting for consistent date serialization - *
- */ - /** * Top-level container for comprehensive Solr collection metrics. * @@ -67,6 +49,13 @@ * } * } * + * @param indexStats Index-related statistics including document counts and segment information + * @param queryStats Query performance metrics from the most recent search operations + * @param cacheStats Cache utilization statistics for query result, document, and filter caches (may + * be null) + * @param handlerStats Request handler performance metrics for select and update operations (may be + * null) + * @param timestamp Timestamp when these metrics were collected, formatted as ISO 8601 * @see IndexStats * @see QueryStats * @see CacheStats @@ -75,19 +64,10 @@ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record SolrMetrics( - /** Index-related statistics including document counts and segment information */ IndexStats indexStats, - - /** Query performance metrics from the most recent search operations */ QueryStats queryStats, - - /** Cache utilization statistics for query result, document, and filter caches (may be null) */ @Nullable CacheStats cacheStats, - - /** Request handler performance metrics for select and update operations (may be null) */ @Nullable HandlerStats handlerStats, - - /** Timestamp when these metrics were collected, formatted as ISO 8601 */ @JsonFormat(shape = JsonFormat.Shape.STRING) Instant timestamp) {} /** @@ -109,18 +89,14 @@ record SolrMetrics( *

High segment counts may indicate the need for index optimization to improve search * performance. The optimal segment count depends on index size and update frequency. * + * @param numDocs Total number of documents in the index (excluding deleted documents) + * @param segmentCount Number of Lucene segments in the index (lower numbers generally indicate + * better performance) * @see org.apache.solr.client.solrj.request.LukeRequest */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -record IndexStats( - /** Total number of documents in the index (excluding deleted documents) */ - @Nullable Integer numDocs, - - /** - * Number of Lucene segments in the index (lower numbers generally indicate better performance) - */ - @Nullable Integer segmentCount) {} +record IndexStats(@Nullable Integer numDocs, @Nullable Integer segmentCount) {} /** * Query execution performance metrics from Solr search operations. @@ -142,22 +118,15 @@ record IndexStats( *

Query time metrics help identify slow queries that may need optimization, while result counts * and scores provide insight into search effectiveness and relevance tuning needs. * + * @param queryTime Time in milliseconds required to execute the most recent query + * @param totalResults Total number of documents matching the query criteria + * @param start Starting position for paginated results (0-based offset) + * @param maxScore Highest relevance score among the returned documents * @see org.apache.solr.client.solrj.response.QueryResponse */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -record QueryStats( - /** Time in milliseconds required to execute the most recent query */ - Integer queryTime, - - /** Total number of documents matching the query criteria */ - Long totalResults, - - /** Starting position for paginated results (0-based offset) */ - Long start, - - /** Highest relevance score among the returned documents */ - @Nullable Float maxScore) {} +record QueryStats(Integer queryTime, Long totalResults, Long start, @Nullable Float maxScore) {} /** * Solr cache utilization statistics across all cache types. @@ -179,18 +148,16 @@ record QueryStats( *

Poor cache hit ratios may indicate undersized caches or query patterns that don't benefit from * caching. Cache evictions suggest memory pressure or cache size optimization needs. * + * @param queryResultCache Performance metrics for the query result cache + * @param documentCache Performance metrics for the document cache + * @param filterCache Performance metrics for the filter cache * @see CacheInfo */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record CacheStats( - /** Performance metrics for the query result cache */ @Nullable CacheInfo queryResultCache, - - /** Performance metrics for the document cache */ @Nullable CacheInfo documentCache, - - /** Performance metrics for the filter cache */ @Nullable CacheInfo filterCache) {} /** @@ -213,26 +180,22 @@ record CacheStats( * *

Optimal cache performance typically shows high hit ratios (>0.80) with minimal evictions. High * eviction rates suggest cache size increases may improve performance. + * + * @param lookups Total number of cache lookup requests + * @param hits Number of successful cache hits + * @param hitratio Cache hit ratio (hits/lookups) - higher values indicate better cache performance + * @param inserts Number of new entries added to the cache + * @param evictions Number of entries removed due to cache size limits (indicates memory pressure) + * @param size Current number of entries stored in the cache */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record CacheInfo( - /** Total number of cache lookup requests */ @Nullable Long lookups, - - /** Number of successful cache hits */ @Nullable Long hits, - - /** Cache hit ratio (hits/lookups) - higher values indicate better cache performance */ @Nullable Float hitratio, - - /** Number of new entries added to the cache */ @Nullable Long inserts, - - /** Number of entries removed due to cache size limits (indicates memory pressure) */ @Nullable Long evictions, - - /** Current number of entries stored in the cache */ @Nullable Long size) {} /** @@ -254,16 +217,13 @@ record CacheInfo( *

Handler metrics help identify bottlenecks in request processing and guide capacity planning * decisions. High error rates or response times indicate potential optimization needs. * + * @param selectHandler Performance metrics for the search/select request handler + * @param updateHandler Performance metrics for the document update request handler * @see HandlerInfo */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -record HandlerStats( - /** Performance metrics for the search/select request handler */ - @Nullable HandlerInfo selectHandler, - - /** Performance metrics for the document update request handler */ - @Nullable HandlerInfo updateHandler) {} +record HandlerStats(@Nullable HandlerInfo selectHandler, @Nullable HandlerInfo updateHandler) {} /** * Detailed performance metrics for individual Solr request handlers. @@ -285,26 +245,22 @@ record HandlerStats( * *

High error rates may indicate system stress or configuration issues. Increasing response times * suggest capacity limits or optimization needs. + * + * @param requests Total number of requests processed by this handler + * @param errors Number of requests that resulted in errors + * @param timeouts Number of requests that exceeded timeout limits + * @param totalTime Cumulative time spent processing all requests (milliseconds) + * @param avgTimePerRequest Average time per request in milliseconds + * @param avgRequestsPerSecond Average throughput in requests per second */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record HandlerInfo( - /** Total number of requests processed by this handler */ @Nullable Long requests, - - /** Number of requests that resulted in errors */ @Nullable Long errors, - - /** Number of requests that exceeded timeout limits */ @Nullable Long timeouts, - - /** Cumulative time spent processing all requests (milliseconds) */ @Nullable Long totalTime, - - /** Average time per request in milliseconds */ @Nullable Float avgTimePerRequest, - - /** Average throughput in requests per second */ @Nullable Float avgRequestsPerSecond) {} /** @@ -336,26 +292,22 @@ record HandlerInfo( * logger.error("Collection unhealthy: " + status.getErrorMessage()); * } * } + * + * @param isHealthy Overall health status - true if collection is operational and responding + * @param errorMessage Detailed error message when isHealthy is false, null when healthy + * @param responseTime Response time in milliseconds for the health check ping request + * @param totalDocuments Total number of documents currently indexed in the collection + * @param lastChecked Timestamp when this health check was performed, formatted as ISO 8601 + * @param collection Name of the collection that was checked */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record SolrHealthStatus( - /** Overall health status - true if collection is operational and responding */ boolean isHealthy, - - /** Detailed error message when isHealthy is false, null when healthy */ @Nullable String errorMessage, - - /** Response time in milliseconds for the health check ping request */ @Nullable Long responseTime, - - /** Total number of documents currently indexed in the collection */ @Nullable Long totalDocuments, - - /** Timestamp when this health check was performed, formatted as ISO 8601 */ @JsonFormat(shape = JsonFormat.Shape.STRING) Instant lastChecked, - - /** Name of the collection that was checked */ String collection) {} /** @@ -364,18 +316,16 @@ record SolrHealthStatus( *

Returned by the {@code create-collection} MCP tool to communicate the outcome of a collection * creation request. On success, {@code success} is {@code true} and {@code createdAt} records when * the operation completed. + * + * @param name Name of the collection that was created + * @param success Whether the collection was successfully created + * @param message Optional message describing the outcome + * @param createdAt Timestamp when the collection was created, formatted as ISO 8601 */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record CollectionCreationResult( - /** Name of the collection that was created */ String name, - - /** Whether the collection was successfully created */ boolean success, - - /** Optional message describing the outcome */ String message, - - /** Timestamp when the collection was created, formatted as ISO 8601 */ @JsonFormat(shape = JsonFormat.Shape.STRING) Instant createdAt) {} diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java index 48df20e6..ae0de2bf 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java +++ b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java @@ -24,6 +24,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.util.StringUtils; import tools.jackson.databind.json.JsonMapper; @@ -85,6 +86,7 @@ */ @Configuration @EnableConfigurationProperties(SolrConfigurationProperties.class) +@ImportRuntimeHints(SolrNativeHints.class) public class SolrConfig { private static final int CONNECTION_TIMEOUT_MS = 10000; @@ -92,7 +94,9 @@ public class SolrConfig { private static final String SOLR_PATH = "solr/"; /** Default constructor used by Spring to instantiate this configuration. */ - public SolrConfig() {} + public SolrConfig() { + // Declared only to carry the Javadoc above; javadoc warns on an implicit constructor. + } /** * Creates and configures a SolrClient bean for Apache Solr communication. @@ -170,7 +174,8 @@ private static SolrClient buildSolrClient( URI parsed = URI.create(properties.url()); String path = parsed.getPath() == null ? "" : parsed.getPath(); if (!path.endsWith("/")) { - path = path + "/"; + // A URL path separator is '/' on every platform; it is not the file separator S1075 assumes. + path = path + "/"; // NOSONAR java:S1075 } if (!path.contains("/" + SOLR_PATH)) { path = path + SOLR_PATH; diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java b/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java index eab54226..57603b90 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java +++ b/src/main/java/org/apache/solr/mcp/server/config/SolrConfigurationProperties.java @@ -139,7 +139,7 @@ private static boolean isAbsoluteHttpUrlWithHost(@Nullable String url) { URI uri; try { uri = URI.create(url); - } catch (IllegalArgumentException ex) { + } catch (IllegalArgumentException _) { return false; } String scheme = uri.getScheme(); diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java b/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java index 7fc28178..db17a355 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java +++ b/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java @@ -30,8 +30,6 @@ import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ImportRuntimeHints; /** * GraalVM native image reachability hints for SolrJ types this project actually uses. @@ -41,18 +39,13 @@ * native-image issues. The hints below cover the narrow remaining surface: response containers and * the {@code NamedList} admin shape returned by the mbeans path. * - *

This class is registered unconditionally — on the JVM path it is a no-op because no runtime - * reflection happens against these types at startup. It is only meaningful when running as a native - * image. + *

{@link SolrConfig} imports this registrar unconditionally — on the JVM path it is a no-op + * because no runtime reflection happens against these types at startup. It is only meaningful when + * running as a native image. * * @see SolrConfig */ -@Configuration -@ImportRuntimeHints(SolrNativeHints.Registrar.class) -public class SolrNativeHints { - - /** Default constructor used by Spring to instantiate this configuration. */ - public SolrNativeHints() {} +class SolrNativeHints implements RuntimeHintsRegistrar { /** * Record types used by {@code @McpTool} methods as responses or parameters. Jackson needs @@ -75,107 +68,105 @@ public SolrNativeHints() {} "org.apache.solr.mcp.server.search.SortClause", "org.apache.solr.mcp.server.schema.SchemaUpdateResult"); - static class Registrar implements RuntimeHintsRegistrar { - @Override - public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - MemberCategory[] categories = { - MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_DECLARED_METHODS, - MemberCategory.ACCESS_DECLARED_FIELDS - }; - - // SolrJ response types - hints.reflection().registerType(QueryResponse.class, categories); - hints.reflection().registerType(UpdateResponse.class, categories); - hints.reflection().registerType(NamedList.class, categories); - hints.reflection().registerType(SimpleOrderedMap.class, categories); - hints.reflection().registerType(SolrDocument.class, categories); - hints.reflection().registerType(SolrDocumentList.class, categories); - - // SolrJ input/indexing types - hints.reflection().registerType(SolrInputDocument.class, categories); - hints.reflection().registerType(SolrInputField.class, categories); - - // SolrJ facet types - hints.reflection().registerType(FacetField.class, categories); - hints.reflection().registerType(FacetField.Count.class, categories); - - // SolrJ / Solr API model types used by CoreAdminResponse.getCoreStatus() - // which deserializes via Jackson reflection. - for (String solrApiType : - List.of( - "org.apache.solr.client.api.model.SolrJerseyResponse", - "org.apache.solr.client.api.model.SolrJerseyResponse$ResponseHeader", - "org.apache.solr.client.api.model.ErrorInfo", - "org.apache.solr.client.api.model.CoreStatusResponse", - "org.apache.solr.client.api.model.CoreStatusResponse$SingleCoreData", - "org.apache.solr.client.api.model.CoreStatusResponse$CloudDetails", - "org.apache.solr.client.api.model.CoreStatusResponse$IndexDetails")) { - hints.reflection().registerTypeIfPresent(classLoader, solrApiType, categories); - } - - // Spring AI MCP annotation internals — MetaUtils reflectively - // instantiates DefaultMetaProvider via its no-arg constructor. - hints - .reflection() - .registerTypeIfPresent( - classLoader, - "org.springframework.ai.mcp.annotation.context.DefaultMetaProvider", - categories); - - // SolrJ schema request types (needed for Jackson's convertValue in native - // image when add-field-types deserializes analyzer trees) - hints - .reflection() - .registerType( - org.apache.solr.client.solrj.request.schema.AnalyzerDefinition.class, categories); - hints - .reflection() - .registerType( - org.apache.solr.client.solrj.request.schema.FieldTypeDefinition.class, categories); - - // SolrJ schema response type — returned by the get-schema MCP tool and - // serialized to JSON by Spring AI for MCP clients. Without reflection - // hints the JSON Spring AI produces in native image is missing the - // fields/fieldTypes/dynamicFields/copyFields arrays, which silently - // breaks any consumer that introspects the schema. - hints - .reflection() - .registerType( - org.apache.solr.client.solrj.response.schema.SchemaRepresentation.class, categories); - - // MCP tool request/response records (some package-private, registered by - // name); request records are deserialized reflectively by Jackson when - // tool arguments are bound - for (String className : MCP_TOOL_RECORDS) { - hints.reflection().registerTypeIfPresent(classLoader, className, categories); - } - - // SolrJ EnvUtils loads these properties files in its static - // initializer; without them getResourceAsStream returns null - // and the throws NullPointerException. - hints.resources().registerPattern("EnvToSyspropMappings.properties"); - hints.resources().registerPattern("DeprecatedSystemPropertyMappings.properties"); - - // Older springaicommunity location of DefaultMetaProvider (Spring AI 1.x - // transitive). Spring AI 2.x relocated this to - // org.springframework.ai.mcp.annotation.context.DefaultMetaProvider, - // which is registered above. Kept defensively via registerTypeIfPresent - // so this is a no-op when the older class isn't on the classpath. - hints - .reflection() - .registerTypeIfPresent( - classLoader, - "org.springaicommunity.mcp.context.DefaultMetaProvider", - MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + MemberCategory[] categories = { + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_DECLARED_METHODS, + MemberCategory.ACCESS_DECLARED_FIELDS + }; + + // SolrJ response types + hints.reflection().registerType(QueryResponse.class, categories); + hints.reflection().registerType(UpdateResponse.class, categories); + hints.reflection().registerType(NamedList.class, categories); + hints.reflection().registerType(SimpleOrderedMap.class, categories); + hints.reflection().registerType(SolrDocument.class, categories); + hints.reflection().registerType(SolrDocumentList.class, categories); + + // SolrJ input/indexing types + hints.reflection().registerType(SolrInputDocument.class, categories); + hints.reflection().registerType(SolrInputField.class, categories); + + // SolrJ facet types + hints.reflection().registerType(FacetField.class, categories); + hints.reflection().registerType(FacetField.Count.class, categories); + + // SolrJ / Solr API model types used by CoreAdminResponse.getCoreStatus() + // which deserializes via Jackson reflection. + for (String solrApiType : + List.of( + "org.apache.solr.client.api.model.SolrJerseyResponse", + "org.apache.solr.client.api.model.SolrJerseyResponse$ResponseHeader", + "org.apache.solr.client.api.model.ErrorInfo", + "org.apache.solr.client.api.model.CoreStatusResponse", + "org.apache.solr.client.api.model.CoreStatusResponse$SingleCoreData", + "org.apache.solr.client.api.model.CoreStatusResponse$CloudDetails", + "org.apache.solr.client.api.model.CoreStatusResponse$IndexDetails")) { + hints.reflection().registerTypeIfPresent(classLoader, solrApiType, categories); + } - // logback-spring.xml is the only logback configuration; Spring Boot - // resolves it by convention. Under AOT the parsed model is replayed - // from META-INF/spring/logback-model, so this hint is belt-and-braces - // for the non-AOT path. Logback's own pre-Spring status output is - // silenced by Main, not by a second configuration file; see - // LoggingConfigurationTest. - hints.resources().registerPattern("logback-spring.xml"); + // Spring AI MCP annotation internals — MetaUtils reflectively + // instantiates DefaultMetaProvider via its no-arg constructor. + hints + .reflection() + .registerTypeIfPresent( + classLoader, + "org.springframework.ai.mcp.annotation.context.DefaultMetaProvider", + categories); + + // SolrJ schema request types (needed for Jackson's convertValue in native + // image when add-field-types deserializes analyzer trees) + hints + .reflection() + .registerType( + org.apache.solr.client.solrj.request.schema.AnalyzerDefinition.class, categories); + hints + .reflection() + .registerType( + org.apache.solr.client.solrj.request.schema.FieldTypeDefinition.class, categories); + + // SolrJ schema response type — returned by the get-schema MCP tool and + // serialized to JSON by Spring AI for MCP clients. Without reflection + // hints the JSON Spring AI produces in native image is missing the + // fields/fieldTypes/dynamicFields/copyFields arrays, which silently + // breaks any consumer that introspects the schema. + hints + .reflection() + .registerType( + org.apache.solr.client.solrj.response.schema.SchemaRepresentation.class, categories); + + // MCP tool request/response records (some package-private, registered by + // name); request records are deserialized reflectively by Jackson when + // tool arguments are bound + for (String className : MCP_TOOL_RECORDS) { + hints.reflection().registerTypeIfPresent(classLoader, className, categories); } + + // SolrJ EnvUtils loads these properties files in its static + // initializer; without them getResourceAsStream returns null + // and the throws NullPointerException. + hints.resources().registerPattern("EnvToSyspropMappings.properties"); + hints.resources().registerPattern("DeprecatedSystemPropertyMappings.properties"); + + // Older springaicommunity location of DefaultMetaProvider (Spring AI 1.x + // transitive). Spring AI 2.x relocated this to + // org.springframework.ai.mcp.annotation.context.DefaultMetaProvider, + // which is registered above. Kept defensively via registerTypeIfPresent + // so this is a no-op when the older class isn't on the classpath. + hints + .reflection() + .registerTypeIfPresent( + classLoader, + "org.springaicommunity.mcp.context.DefaultMetaProvider", + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + + // logback-spring.xml is the only logback configuration; Spring Boot + // resolves it by convention. Under AOT the parsed model is replayed + // from META-INF/spring/logback-model, so this hint is belt-and-braces + // for the non-AOT path. Logback's own pre-Spring status output is + // silenced by Main, not by a second configuration file; see + // LoggingConfigurationTest. + hints.resources().registerPattern("logback-spring.xml"); } } diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java index c48145d4..9571377a 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java @@ -218,14 +218,14 @@ private void processFieldValue(SolrInputDocument doc, JsonNode value, String fie * be added */ private void processArrayField(SolrInputDocument doc, JsonNode arrayValue, String fieldName) { - List values = new ArrayList<>(); - for (JsonNode item : arrayValue) { - // Skip objects and nested arrays alike: neither has a scalar - // representation, and asString() on a container node throws. - if (!item.isObject() && !item.isArray()) { - values.add(convertJsonValue(item)); - } - } + // Skip objects and nested arrays alike: neither has a scalar + // representation, and asString() on a container node throws. + List values = + arrayValue + .valueStream() + .filter(item -> !item.isContainer()) + .map(this::convertJsonValue) + .toList(); if (!values.isEmpty()) { doc.addField(fieldName, values); } diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java index bff112b7..8557dec3 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/MarkdownDocumentCreator.java @@ -33,7 +33,6 @@ import org.commonmark.node.Code; import org.commonmark.node.Heading; import org.commonmark.node.Node; -import org.commonmark.node.SourceSpan; import org.commonmark.node.Text; import org.commonmark.parser.IncludeSourceSpans; import org.commonmark.parser.Parser; @@ -136,7 +135,9 @@ public MarkdownDocumentCreator() { options, new Resolver() { @Override - protected void addImplicitResolvers() {} + protected void addImplicitResolvers() { + // Registers none, so every scalar stays a string. + } }); this.textContentRenderer = TextContentRenderer.builder().build(); } @@ -164,11 +165,7 @@ public List create(String markdown) throws DocumentProcessing return List.of(); } - List documents = new ArrayList<>(); - for (String record : splitRecords(markdown)) { - documents.add(createOne(record)); - } - return List.copyOf(documents); + return splitRecords(markdown).stream().map(this::createOne).toList(); } /** Creates the single document described by one record. */ @@ -291,10 +288,10 @@ private void addFrontMatterFields(String markdown, Node document, SolrInputDocum if (!(firstChild instanceof YamlFrontMatterBlock block)) { return; } - List lines = new ArrayList<>(); - for (SourceSpan span : block.getSourceSpans()) { - lines.add(markdown.substring(span.getInputIndex(), span.getInputIndex() + span.getLength())); - } + List lines = + block.getSourceSpans().stream() + .map(s -> markdown.substring(s.getInputIndex(), s.getInputIndex() + s.getLength())) + .toList(); // The first and last spans are the --- delimiters String text = String.join("\n", lines.subList(1, Math.max(1, lines.size() - 1))); Object data; @@ -311,7 +308,9 @@ private void addFrontMatterFields(String markdown, Node document, SolrInputDocum private static void addValue(SolrInputDocument doc, String fieldName, @Nullable Object value) { switch (value) { - case null -> {} + case null -> { + // A key with no value, such as `tags:`, adds no field. + } case Map nested -> nested.forEach((key, inner) -> addValue(doc, fieldName + "_" + key, inner)); case Iterable values -> values.forEach(element -> addValue(doc, fieldName, element)); diff --git a/src/test/java/org/apache/solr/mcp/server/BuildInfoReader.java b/src/test/java/org/apache/solr/mcp/server/BuildInfoReader.java index 60bccf5f..6d72be3c 100644 --- a/src/test/java/org/apache/solr/mcp/server/BuildInfoReader.java +++ b/src/test/java/org/apache/solr/mcp/server/BuildInfoReader.java @@ -99,7 +99,7 @@ public static String getVersion() { */ public static String getDockerImageName() { String suffix = System.getProperty("solr.mcp.docker.image.tag.suffix", ""); - return String.format("%s:%s%s", getArtifact(), getVersion(), suffix); + return getArtifact() + ":" + getVersion() + suffix; } /** @@ -108,6 +108,6 @@ public static String getDockerImageName() { * @return JAR file name (e.g., "solr-mcp-1.0.0-SNAPSHOT.jar") */ public static String getJarFileName() { - return String.format("%s-%s.jar", getArtifact(), getVersion()); + return getArtifact() + "-" + getVersion() + ".jar"; } } diff --git a/src/test/java/org/apache/solr/mcp/server/ClientHttp.java b/src/test/java/org/apache/solr/mcp/server/ClientHttp.java index b482d5c2..15bdad99 100644 --- a/src/test/java/org/apache/solr/mcp/server/ClientHttp.java +++ b/src/test/java/org/apache/solr/mcp/server/ClientHttp.java @@ -22,7 +22,7 @@ @Disabled("Enable only when MCP server is running in http mode") public class ClientHttp { - public static void main(String[] args) { + static void main() { var transport = HttpClientStreamableHttpTransport.builder("http://localhost:8080").build(); new SampleClient(transport).run(); } diff --git a/src/test/java/org/apache/solr/mcp/server/ClientStdio.java b/src/test/java/org/apache/solr/mcp/server/ClientStdio.java index 60055305..992e041f 100644 --- a/src/test/java/org/apache/solr/mcp/server/ClientStdio.java +++ b/src/test/java/org/apache/solr/mcp/server/ClientStdio.java @@ -27,7 +27,7 @@ public class ClientStdio { static void main() { - String jarName = String.format("build/libs/%s", BuildInfoReader.getJarFileName()); + String jarName = "build/libs/" + BuildInfoReader.getJarFileName(); var stdioParams = ServerParameters.builder("java").args("-jar", jarName).build(); diff --git a/src/test/java/org/apache/solr/mcp/server/MainTest.java b/src/test/java/org/apache/solr/mcp/server/MainTest.java index 96e19e21..e7a16d61 100644 --- a/src/test/java/org/apache/solr/mcp/server/MainTest.java +++ b/src/test/java/org/apache/solr/mcp/server/MainTest.java @@ -16,13 +16,17 @@ */ package org.apache.solr.mcp.server; +import static org.assertj.core.api.Assertions.assertThat; + import org.apache.solr.mcp.server.collection.CollectionService; import org.apache.solr.mcp.server.indexing.IndexingService; import org.apache.solr.mcp.server.schema.SchemaService; import org.apache.solr.mcp.server.search.SearchService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledInNativeImage; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -36,6 +40,8 @@ @DisabledInNativeImage class MainTest { + @Autowired private ApplicationContext context; + @MockitoBean private SearchService searchService; @MockitoBean private IndexingService indexingService; @@ -46,6 +52,10 @@ class MainTest { @Test void contextLoads() { - // Context loading test - all services are mocked to prevent Solr API calls + // The context starts with every Solr-calling service replaced by its mock + assertThat(context.getBean(SearchService.class)).isSameAs(searchService); + assertThat(context.getBean(IndexingService.class)).isSameAs(indexingService); + assertThat(context.getBean(CollectionService.class)).isSameAs(collectionService); + assertThat(context.getBean(SchemaService.class)).isSameAs(schemaService); } } diff --git a/src/test/java/org/apache/solr/mcp/server/SampleDataRuntimeHints.java b/src/test/java/org/apache/solr/mcp/server/SampleDataRuntimeHints.java index 25baf758..479a3188 100644 --- a/src/test/java/org/apache/solr/mcp/server/SampleDataRuntimeHints.java +++ b/src/test/java/org/apache/solr/mcp/server/SampleDataRuntimeHints.java @@ -42,9 +42,6 @@ */ public class SampleDataRuntimeHints implements TestRuntimeHintsRegistrar { - /** Default constructor used by Spring's AOT services loader. */ - public SampleDataRuntimeHints() {} - @Override public void registerHints(RuntimeHints hints, Class testClass, ClassLoader classLoader) { hints diff --git a/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java b/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java index 78bfba63..a8780a93 100644 --- a/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java +++ b/src/test/java/org/apache/solr/mcp/server/config/SolrNativeHintsTest.java @@ -27,8 +27,8 @@ import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; /** - * Verifies that {@link SolrNativeHints.Registrar} registers the reflection and resource hints the - * native image depends on. + * Verifies that {@link SolrNativeHints} registers the reflection and resource hints the native + * image depends on. * *

The hints are otherwise only exercised by {@code nativeTest -Pnative} (a full GraalVM build), * so an accidentally removed registration would surface as a hard-to-diagnose native-only startup @@ -41,7 +41,7 @@ class SolrNativeHintsTest { @BeforeEach void registerHints() { - new SolrNativeHints.Registrar().registerHints(hints, getClass().getClassLoader()); + new SolrNativeHints().registerHints(hints, getClass().getClassLoader()); } @Test diff --git a/src/test/java/org/apache/solr/mcp/server/containerization/DockerImageStdioIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/containerization/DockerImageStdioIntegrationTest.java index 7e07c96e..6ccfdbbd 100644 --- a/src/test/java/org/apache/solr/mcp/server/containerization/DockerImageStdioIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/containerization/DockerImageStdioIntegrationTest.java @@ -115,13 +115,16 @@ class DockerImageStdioIntegrationTest { .withStartupTimeout(Duration.ofSeconds(60)); @BeforeAll - static void setup() throws InterruptedException { + static void setup() { log.info("Solr container started. Internal URL: http://solr:8983/solr/"); log.info("MCP Server container starting. Waiting for initialization..."); - // Give the MCP server a few seconds to initialize + // Give the MCP server a few seconds to initialize, failing early if it exits. // In STDIO mode, the app runs but doesn't produce logs we can monitor - Thread.sleep(5000); + await() + .during(Duration.ofSeconds(5)) + .atMost(Duration.ofSeconds(10)) + .until(mcpServerContainer::isRunning); log.info("Initialization wait complete. Beginning tests."); } diff --git a/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java b/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java index 4e887e9d..84c127dc 100644 --- a/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java +++ b/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java @@ -42,7 +42,6 @@ *

    *
  • Spans are created for @Observed methods *
  • Span attributes are correctly set - *
  • Span hierarchy is correct (parent-child relationships) *
  • Span names follow conventions *
* @@ -139,64 +138,6 @@ void shouldIncludeSpanAttributes() { }); } - @Test - void shouldCreateSpanHierarchy() { - // When: We execute a complex operation that triggers multiple spans - try { - searchService.search("test_collection", "*:*", null, null, null, null, null); - } catch (Exception _) { - // Ignore errors - } - - // Then: We should see spans created - await() - .atMost(5, TimeUnit.SECONDS) - .untilAsserted( - () -> { - var spans = tracer.getSpans(); - assertThat(spans).as("Should have created spans").isNotEmpty(); - }); - } - - @Test - void shouldSetCorrectSpanKind() { - // When: We execute a service method - try { - searchService.search("test_collection", "*:*", null, null, null, null, null); - } catch (Exception _) { - // Ignore errors - } - - // Then: Spans should have appropriate span kinds - await() - .atMost(5, TimeUnit.SECONDS) - .untilAsserted( - () -> { - var spans = tracer.getSpans(); - assertThat(spans).as("Should have created spans").isNotEmpty(); - }); - } - - @Test - void shouldIncludeServiceNameInResource() { - // When: We execute any operation - try { - searchService.search("test_collection", "*:*", null, null, null, null, null); - } catch (Exception _) { - // Ignore errors - } - - // Then: Spans should be created (service name is in resource attributes in - // OpenTelemetry) - await() - .atMost(5, TimeUnit.SECONDS) - .untilAsserted( - () -> { - var spans = tracer.getSpans(); - assertThat(spans).as("Should have created spans").isNotEmpty(); - }); - } - @Test void shouldRecordSpanDuration() { // When: We execute an operation diff --git a/src/test/java/org/apache/solr/mcp/server/schema/SchemaServiceTest.java b/src/test/java/org/apache/solr/mcp/server/schema/SchemaServiceTest.java index dd59e4b5..dba24d69 100644 --- a/src/test/java/org/apache/solr/mcp/server/schema/SchemaServiceTest.java +++ b/src/test/java/org/apache/solr/mcp/server/schema/SchemaServiceTest.java @@ -158,12 +158,9 @@ void testConstructor() { @Test void addFields_blankCollection_throws() { - assertThrows( - IllegalArgumentException.class, - () -> schemaService.addFields("", List.of(Map.of("name", "x", "type", "string")))); - assertThrows( - IllegalArgumentException.class, - () -> schemaService.addFields(" ", List.of(Map.of("name", "x", "type", "string")))); + List> fields = List.of(Map.of("name", "x", "type", "string")); + assertThrows(IllegalArgumentException.class, () -> schemaService.addFields("", fields)); + assertThrows(IllegalArgumentException.class, () -> schemaService.addFields(" ", fields)); } @Test @@ -205,23 +202,15 @@ void addFields_solrThrows_propagates() throws Exception { when(solrClient.request(any(SolrRequest.class), eq("col"))) .thenThrow(new SolrServerException("simulated")); - assertThrows( - SolrServerException.class, - () -> schemaService.addFields("col", List.of(Map.of("name", "x", "type", "string")))); + List> fields = List.of(Map.of("name", "x", "type", "string")); + assertThrows(SolrServerException.class, () -> schemaService.addFields("col", fields)); } @Test void addFieldTypes_blankCollection_throws() { - assertThrows( - IllegalArgumentException.class, - () -> - schemaService.addFieldTypes( - "", List.of(Map.of("name", "x", "class", "solr.StrField")))); - assertThrows( - IllegalArgumentException.class, - () -> - schemaService.addFieldTypes( - " ", List.of(Map.of("name", "x", "class", "solr.StrField")))); + List> types = List.of(Map.of("name", "x", "class", "solr.StrField")); + assertThrows(IllegalArgumentException.class, () -> schemaService.addFieldTypes("", types)); + assertThrows(IllegalArgumentException.class, () -> schemaService.addFieldTypes(" ", types)); } @Test diff --git a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java index 1adb6de3..862a2682 100644 --- a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java @@ -261,18 +261,13 @@ void searchWithUndefinedFieldInQueryReturnsGetSchemaHint() { @Test void searchWithUndefinedFieldInFilterQueryReturnsGetSchemaHint() { + List filterQueries = List.of("definitely_not_a_field:value"); IllegalArgumentException e = assertThrows( IllegalArgumentException.class, () -> searchService.search( - COLLECTION_NAME, - "*:*", - List.of("definitely_not_a_field:value"), - null, - null, - null, - null)); + COLLECTION_NAME, "*:*", filterQueries, null, null, null, null)); assertTrue( e.getMessage().contains(SearchService.GET_SCHEMA_HINT_FORMAT.formatted(COLLECTION_NAME)), () -> "expected get-schema hint, got: " + e.getMessage()); @@ -282,18 +277,12 @@ void searchWithUndefinedFieldInFilterQueryReturnsGetSchemaHint() { /** Faceting words it differently: {@code undefined field: "name"}. */ @Test void searchWithUndefinedFacetFieldReturnsGetSchemaHint() { + List facetFields = List.of("definitely_not_a_field"); IllegalArgumentException e = assertThrows( IllegalArgumentException.class, () -> - searchService.search( - COLLECTION_NAME, - "*:*", - null, - List.of("definitely_not_a_field"), - null, - null, - null)); + searchService.search(COLLECTION_NAME, "*:*", null, facetFields, null, null, null)); assertTrue( e.getMessage().contains(SearchService.GET_SCHEMA_HINT_FORMAT.formatted(COLLECTION_NAME)), () -> "expected get-schema hint, got: " + e.getMessage()); diff --git a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceTest.java b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceTest.java index 53124cf0..e5c697bd 100644 --- a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceTest.java +++ b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceTest.java @@ -136,18 +136,11 @@ void search_WithMixedBlankAndValidOptions_ShouldPreserveValidValues() throws Exc @Test void search_WithInvalidNonblankSortOrder_ShouldStillFail() { SearchService localService = new SearchService(mock(SolrClient.class)); + List sort = List.of(new SortClause("price", "descending")); IllegalArgumentException e = assertThrows( IllegalArgumentException.class, - () -> - localService.search( - "test_collection", - null, - null, - null, - List.of(new SortClause("price", "descending")), - null, - null)); + () -> localService.search("test_collection", null, null, null, sort, null, null)); assertTrue(e.getMessage().contains("'asc' or 'desc'")); } @@ -198,12 +191,12 @@ void search_FacetConversionFailure_ShouldNotReturnSuccessfulSearch() throws Exce when(mockResponse.getFacetFields()).thenThrow(failure); when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenReturn(mockResponse); SearchService localService = new SearchService(mockClient); + List facetFields = List.of("platform"); IllegalStateException e = assertThrows( IllegalStateException.class, () -> - localService.search( - "test_collection", "*:*", null, List.of("platform"), null, null, null)); + localService.search("test_collection", "*:*", null, facetFields, null, null, null)); assertFalse(e.getMessage().contains("ClassCastException")); assertFalse(e.getMessage().contains("NamedList")); assertTrue(e.getMessage().contains("facetFields")); diff --git a/src/test/java/org/apache/solr/mcp/server/search/SortClauseTest.java b/src/test/java/org/apache/solr/mcp/server/search/SortClauseTest.java index f3a64a5b..8e3aa353 100644 --- a/src/test/java/org/apache/solr/mcp/server/search/SortClauseTest.java +++ b/src/test/java/org/apache/solr/mcp/server/search/SortClauseTest.java @@ -47,17 +47,16 @@ void defaultsMissingOrderToAscending() { @Test void rejectsInvalidOrderNamingTheValue() { + SortClause clause = new SortClause("price", "descending"); IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - () -> new SortClause("price", "descending").toSolrSortClause()); + assertThrows(IllegalArgumentException.class, clause::toSolrSortClause); assertTrue(e.getMessage().contains("descending")); assertTrue(e.getMessage().contains("price")); } @Test void rejectsMissingField() { - assertThrows( - IllegalArgumentException.class, () -> new SortClause(" ", "asc").toSolrSortClause()); + SortClause clause = new SortClause(" ", "asc"); + assertThrows(IllegalArgumentException.class, clause::toSolrSortClause); } } From eccbb9389fe5fdc8a546bf4ee23a1120089274d4 Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Mon, 26 Oct 2026 17:25:55 -0400 Subject: [PATCH 2/3] build: opt Jib tasks out of the configuration cache jibDockerBuild fails with "Invocation of 'Task.project' at execution time is unsupported with the configuration cache" now that org.gradle.configuration-cache=true. Jib 3.5.4 reads Task.project when it runs. Mark the Jib tasks not compatible so they run without the cache and the rest of the build keeps using it. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Aditya Parikh (cherry picked from commit 965645e0e1024eef1b34f8e68f389f0738776049) --- build.gradle.kts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index a9ab9a75..36072d6a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -579,6 +579,13 @@ jib { } } +// Jib's tasks read Task.project at execution time, which the configuration cache rejects. +// Opt them out so jibDockerBuild/jib work with org.gradle.configuration-cache=true; every +// other task keeps using the cache. +tasks.withType().configureEach { + notCompatibleWithConfigurationCache("Jib reads Task.project at execution time") +} + tasks.bootBuildImage { if (nativeBuild) { imageName.set("solr-mcp:$version-native-$nativeProfile") From 9740b109bc316e9b0bd21266ddec774130be719e Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Wed, 28 Oct 2026 12:40:10 -0400 Subject: [PATCH 3/3] docs: add SonarCloud badges to README Signed-off-by: Aditya Parikh Co-authored-by: Junie --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index ac1d7a66..53e3e9a9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ [![Project Status: Incubating](https://img.shields.io/badge/status-incubating-yellow.svg)](https://github.com/apache/solr-mcp) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=alert_status)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=security_rating)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=reliability_rating)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=sqale_rating)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=bugs)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=vulnerabilities)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=code_smells)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=ncloc)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) +[![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=apache_solr-mcp&metric=duplicated_lines_density)](https://sonarcloud.io/project/overview?id=apache_solr-mcp) # Solr MCP Server