From 487f09087d1c5a4668ff2b8a880f03ad02b1b5bb Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Sat, 28 Feb 2026 23:38:36 +0000 Subject: [PATCH 1/8] feat(config): switch Solr wire format from JavaBin to JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the default BinaryResponseParser (wt=javabin) with a custom JsonResponseParser (wt=json) for future-proofing and improved debuggability. The JsonResponseParser converts Solr's JSON response envelope into the NamedList tree that SolrJ typed response classes expect: - JSON objects → SimpleOrderedMap (extends NamedList, implements Map, satisfying both QueryResponse's NamedList casts and SchemaResponse's Map cast) - JSON objects with numFound+docs → SolrDocumentList - Flat alternating arrays [String, non-String, ...] → SimpleOrderedMap (Solr's json.nl=flat encoding for facet counts) - All other arrays → List - Decimal numbers → Float (matching JavaBin's float type, required by SchemaResponse's (Float) version cast) - Small integers → Integer, large integers → Long Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: adityamparikh --- .../mcp/server/config/JsonResponseParser.java | 207 ++++++++++++++++++ .../solr/mcp/server/config/SolrConfig.java | 5 +- 2 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java diff --git a/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java b/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java new file mode 100644 index 0000000..97f5053 --- /dev/null +++ b/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.mcp.server.config; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import org.apache.solr.client.solrj.ResponseParser; +import org.apache.solr.common.SolrDocument; +import org.apache.solr.common.SolrDocumentList; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.SimpleOrderedMap; + +/** + * SolrJ {@link ResponseParser} that requests JSON wire format ({@code wt=json}) + * and converts the Solr JSON response into the {@link NamedList} object tree + * that SolrJ's typed response classes + * ({@link org.apache.solr.client.solrj.response.QueryResponse}, + * {@link org.apache.solr.client.solrj.response.LukeResponse}, etc.) expect + * internally. + * + *

+ * This allows the application to keep all existing SolrJ response handling + * unchanged while moving off the JavaBin ({@code wt=javabin}) wire format. + * + *

+ * Structural conversions: + *

    + *
  • JSON objects → {@link NamedList} (preserving key order)
  • + *
  • JSON objects containing {@code numFound} + {@code docs} → + * {@link SolrDocumentList}
  • + *
  • JSON arrays with alternating {@code [String, non-String, ...]} pairs → + * {@link NamedList} (Solr's {@code json.nl=flat} facet encoding)
  • + *
  • All other JSON arrays → {@link List}
  • + *
  • JSON integers → {@link Integer} or {@link Long} (by value size)
  • + *
  • JSON decimals → {@link Double}
  • + *
  • JSON booleans → {@link Boolean}
  • + *
  • JSON strings → {@link String}
  • + *
+ * + *

+ * Flat NamedList detection: Solr serializes facet counts as + * flat arrays using {@code json.nl=flat} (the default). An array is treated as + * a flat NamedList when every even-indexed element is a {@link String} and + * every odd-indexed element is a non-{@link String} value. This reliably + * distinguishes {@code ["term", 5, "term2", 3]} (facet NamedList) from + * {@code ["col1", "col2"]} (plain string list). + */ +class JsonResponseParser extends ResponseParser { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Override + public String getWriterType() { + return "json"; + } + + @Override + public String getContentType() { + return "application/json; charset=UTF-8"; + } + + @Override + public NamedList processResponse(InputStream body, String encoding) { + try { + return toNamedList(MAPPER.readTree(body)); + } catch (IOException e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed to parse Solr JSON response", e); + } + } + + @Override + public NamedList processResponse(Reader reader) { + try { + return toNamedList(MAPPER.readTree(reader)); + } catch (IOException e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed to parse Solr JSON response", e); + } + } + + private SimpleOrderedMap toNamedList(JsonNode objectNode) { + SimpleOrderedMap result = new SimpleOrderedMap<>(); + objectNode.fields().forEachRemaining(entry -> result.add(entry.getKey(), convertValue(entry.getValue()))); + return result; + } + + private Object convertValue(JsonNode node) { + if (node.isNull()) + return null; + if (node.isBoolean()) + return node.booleanValue(); + if (node.isTextual()) + return node.textValue(); + if (node.isInt()) + return node.intValue(); + if (node.isLong()) + return node.longValue(); + if (node.isDouble() || node.isFloat()) + return node.floatValue(); + if (node.isObject()) + return convertObject(node); + if (node.isArray()) + return convertArray(node); + return node.asText(); + } + + private Object convertObject(JsonNode node) { + // Detect a Solr query result set by the presence of numFound + docs + if (node.has("numFound") && node.has("docs")) { + return toSolrDocumentList(node); + } + return toNamedList(node); + } + + private Object convertArray(JsonNode arrayNode) { + // Detect Solr's flat NamedList encoding: [String, non-String, String, + // non-String, ...] + // Used for facet counts (json.nl=flat default). Distinguished from plain string + // arrays + // by requiring odd-indexed elements to be non-string values. + if (isFlatNamedList(arrayNode)) { + return flatArrayToNamedList(arrayNode); + } + List list = new ArrayList<>(arrayNode.size()); + arrayNode.forEach(element -> list.add(convertValue(element))); + return list; + } + + /** + * Returns true when the array has even length, every even-indexed element is a + * string (the key), and every odd-indexed element is NOT a string (the value). + * This heuristic correctly identifies Solr facet data like + * {@code ["fantasy", 10, "scifi", 5]} while rejecting plain string arrays like + * {@code ["col1", "col2"]}. + */ + private boolean isFlatNamedList(JsonNode arrayNode) { + int size = arrayNode.size(); + if (size == 0 || size % 2 != 0) + return false; + for (int i = 0; i < size; i += 2) { + if (!arrayNode.get(i).isTextual()) + return false; // key must be string + if (arrayNode.get(i + 1).isTextual()) + return false; // value must not be string + } + return true; + } + + private SimpleOrderedMap flatArrayToNamedList(JsonNode arrayNode) { + SimpleOrderedMap result = new SimpleOrderedMap<>(); + for (int i = 0; i < arrayNode.size(); i += 2) { + result.add(arrayNode.get(i).textValue(), convertValue(arrayNode.get(i + 1))); + } + return result; + } + + private SolrDocumentList toSolrDocumentList(JsonNode node) { + SolrDocumentList list = new SolrDocumentList(); + list.setNumFound(node.get("numFound").longValue()); + list.setStart(node.path("start").longValue()); + JsonNode numFoundExact = node.get("numFoundExact"); + if (numFoundExact != null && !numFoundExact.isNull()) { + list.setNumFoundExact(numFoundExact.booleanValue()); + } + JsonNode maxScore = node.get("maxScore"); + if (maxScore != null && !maxScore.isNull()) { + list.setMaxScore(maxScore.floatValue()); + } + node.get("docs").forEach(doc -> list.add(toSolrDocument(doc))); + return list; + } + + private SolrDocument toSolrDocument(JsonNode node) { + SolrDocument doc = new SolrDocument(); + node.fields().forEachRemaining(entry -> { + JsonNode val = entry.getValue(); + if (val.isArray()) { + // Multi-valued field — always a plain list, never a flat NamedList + List values = new ArrayList<>(val.size()); + val.forEach(v -> values.add(convertValue(v))); + doc.setField(entry.getKey(), values); + } else { + doc.setField(entry.getKey(), convertValue(val)); + } + }); + return doc; + } +} 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 7359ccf..cefa735 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 @@ -186,8 +186,9 @@ SolrClient solrClient(SolrConfigurationProperties properties) { } } - // Use with explicit base URL + // Use with explicit base URL; JSON wire format replaces the JavaBin default return new Http2SolrClient.Builder(url).withConnectionTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) - .withIdleTimeout(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS).build(); + .withIdleTimeout(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS).withResponseParser(new JsonResponseParser()) + .build(); } } From a387b19f00d91d752d16594cef1fd937cb346924 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Thu, 5 Mar 2026 23:25:03 -0500 Subject: [PATCH 2/8] refactor(config): make JsonResponseParser a Spring Bean Extract JsonResponseParser instantiation into a dedicated @Bean method so it can be injected as a dependency into solrClient(), making the wiring explicit and enabling overriding in tests. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: adityamparikh --- .../org/apache/solr/mcp/server/config/SolrConfig.java | 9 +++++++-- .../apache/solr/mcp/server/config/SolrConfigTest.java | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) 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 cefa735..95f8104 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 @@ -168,7 +168,12 @@ public class SolrConfig { * @see SolrConfigurationProperties#url() */ @Bean - SolrClient solrClient(SolrConfigurationProperties properties) { + JsonResponseParser jsonResponseParser() { + return new JsonResponseParser(); + } + + @Bean + SolrClient solrClient(SolrConfigurationProperties properties, JsonResponseParser jsonResponseParser) { String url = properties.url(); // Ensure URL is properly formatted for Solr @@ -188,7 +193,7 @@ SolrClient solrClient(SolrConfigurationProperties properties) { // Use with explicit base URL; JSON wire format replaces the JavaBin default return new Http2SolrClient.Builder(url).withConnectionTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) - .withIdleTimeout(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS).withResponseParser(new JsonResponseParser()) + .withIdleTimeout(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS).withResponseParser(jsonResponseParser) .build(); } } diff --git a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java index 342ddd0..e14c1fb 100644 --- a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java +++ b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java @@ -81,7 +81,7 @@ void testUrlNormalization(String inputUrl, String expectedUrl) { SolrConfig solrConfig = new SolrConfig(); // Test URL normalization - SolrClient client = solrConfig.solrClient(testProperties); + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser()); assertNotNull(client); var httpClient = assertInstanceOf(Http2SolrClient.class, client); @@ -101,7 +101,7 @@ void testUrlWithoutTrailingSlash() { SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983"); SolrConfig solrConfig = new SolrConfig(); - SolrClient client = solrConfig.solrClient(testProperties); + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser()); Http2SolrClient httpClient = (Http2SolrClient) client; // Should add trailing slash and solr path @@ -120,7 +120,7 @@ void testUrlWithTrailingSlashButNoSolrPath() { SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/"); SolrConfig solrConfig = new SolrConfig(); - SolrClient client = solrConfig.solrClient(testProperties); + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser()); Http2SolrClient httpClient = (Http2SolrClient) client; // Should add solr path to existing trailing slash @@ -139,7 +139,7 @@ void testUrlWithSolrPathButNoTrailingSlash() { SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/solr"); SolrConfig solrConfig = new SolrConfig(); - SolrClient client = solrConfig.solrClient(testProperties); + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser()); Http2SolrClient httpClient = (Http2SolrClient) client; // Should add trailing slash @@ -158,7 +158,7 @@ void testUrlAlreadyProperlyFormatted() { SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/solr/"); SolrConfig solrConfig = new SolrConfig(); - SolrClient client = solrConfig.solrClient(testProperties); + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser()); Http2SolrClient httpClient = (Http2SolrClient) client; // Should remain unchanged From ca805998f4778f742c35bd9d7660ea345645661a Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Fri, 6 Mar 2026 19:27:46 -0500 Subject: [PATCH 3/8] refactor(config): inject Spring's ObjectMapper into JsonResponseParser Replace the static new ObjectMapper() with Spring's auto-configured ObjectMapper bean injected via constructor. Use MediaType.APPLICATION_JSON_VALUE for the content type constant instead of a raw string literal. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: adityamparikh --- .../solr/mcp/server/config/JsonResponseParser.java | 13 +++++++++---- .../apache/solr/mcp/server/config/SolrConfig.java | 5 +++-- .../solr/mcp/server/config/SolrConfigTest.java | 11 ++++++----- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java b/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java index 97f5053..b57daf8 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java +++ b/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java @@ -29,6 +29,7 @@ import org.apache.solr.common.SolrException; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.SimpleOrderedMap; +import org.springframework.http.MediaType; /** * SolrJ {@link ResponseParser} that requests JSON wire format ({@code wt=json}) @@ -67,7 +68,11 @@ */ class JsonResponseParser extends ResponseParser { - private static final ObjectMapper MAPPER = new ObjectMapper(); + private final ObjectMapper mapper; + + JsonResponseParser(ObjectMapper mapper) { + this.mapper = mapper; + } @Override public String getWriterType() { @@ -76,13 +81,13 @@ public String getWriterType() { @Override public String getContentType() { - return "application/json; charset=UTF-8"; + return MediaType.APPLICATION_JSON_VALUE; } @Override public NamedList processResponse(InputStream body, String encoding) { try { - return toNamedList(MAPPER.readTree(body)); + return toNamedList(mapper.readTree(body)); } catch (IOException e) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed to parse Solr JSON response", e); } @@ -91,7 +96,7 @@ public NamedList processResponse(InputStream body, String encoding) { @Override public NamedList processResponse(Reader reader) { try { - return toNamedList(MAPPER.readTree(reader)); + return toNamedList(mapper.readTree(reader)); } catch (IOException e) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed to parse Solr JSON response", e); } 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 95f8104..ed2cf88 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 @@ -16,6 +16,7 @@ */ package org.apache.solr.mcp.server.config; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.concurrent.TimeUnit; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.impl.Http2SolrClient; @@ -168,8 +169,8 @@ public class SolrConfig { * @see SolrConfigurationProperties#url() */ @Bean - JsonResponseParser jsonResponseParser() { - return new JsonResponseParser(); + JsonResponseParser jsonResponseParser(ObjectMapper objectMapper) { + return new JsonResponseParser(objectMapper); } @Bean diff --git a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java index e14c1fb..086c459 100644 --- a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java +++ b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.*; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.impl.Http2SolrClient; import org.apache.solr.mcp.server.TestcontainersConfiguration; @@ -81,7 +82,7 @@ void testUrlNormalization(String inputUrl, String expectedUrl) { SolrConfig solrConfig = new SolrConfig(); // Test URL normalization - SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser()); + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(new ObjectMapper())); assertNotNull(client); var httpClient = assertInstanceOf(Http2SolrClient.class, client); @@ -101,7 +102,7 @@ void testUrlWithoutTrailingSlash() { SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983"); SolrConfig solrConfig = new SolrConfig(); - SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser()); + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(new ObjectMapper())); Http2SolrClient httpClient = (Http2SolrClient) client; // Should add trailing slash and solr path @@ -120,7 +121,7 @@ void testUrlWithTrailingSlashButNoSolrPath() { SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/"); SolrConfig solrConfig = new SolrConfig(); - SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser()); + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(new ObjectMapper())); Http2SolrClient httpClient = (Http2SolrClient) client; // Should add solr path to existing trailing slash @@ -139,7 +140,7 @@ void testUrlWithSolrPathButNoTrailingSlash() { SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/solr"); SolrConfig solrConfig = new SolrConfig(); - SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser()); + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(new ObjectMapper())); Http2SolrClient httpClient = (Http2SolrClient) client; // Should add trailing slash @@ -158,7 +159,7 @@ void testUrlAlreadyProperlyFormatted() { SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/solr/"); SolrConfig solrConfig = new SolrConfig(); - SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser()); + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(new ObjectMapper())); Http2SolrClient httpClient = (Http2SolrClient) client; // Should remain unchanged From 4b08938fb3edeb075e520d53c15fc46df913d107 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Fri, 6 Mar 2026 19:57:42 -0500 Subject: [PATCH 4/8] test(config): use @JsonTest for URL normalization tests to get Spring's ObjectMapper Extract URL normalization tests from SolrConfigTest into a dedicated SolrConfigUrlNormalizationTest annotated with @JsonTest, so Spring's auto-configured ObjectMapper is injected rather than using new ObjectMapper(). Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: adityamparikh --- .../mcp/server/config/SolrConfigTest.java | 106 --------------- .../SolrConfigUrlNormalizationTest.java | 126 ++++++++++++++++++ 2 files changed, 126 insertions(+), 106 deletions(-) create mode 100644 src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java diff --git a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java index 086c459..ce109c6 100644 --- a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java +++ b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java @@ -18,13 +18,10 @@ import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.impl.Http2SolrClient; import org.apache.solr.mcp.server.TestcontainersConfiguration; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; @@ -68,107 +65,4 @@ void testSolrConfigurationProperties() { properties.url()); } - @ParameterizedTest - @CsvSource({"http://localhost:8983, http://localhost:8983/solr", - "http://localhost:8983/, http://localhost:8983/solr", - "http://localhost:8983/solr, http://localhost:8983/solr", - "http://localhost:8983/solr/, http://localhost:8983/solr", - "http://localhost:8983/custom/solr/, http://localhost:8983/custom/solr"}) - void testUrlNormalization(String inputUrl, String expectedUrl) { - // Create a test properties object - SolrConfigurationProperties testProperties = new SolrConfigurationProperties(inputUrl); - - // Create SolrConfig instance - SolrConfig solrConfig = new SolrConfig(); - - // Test URL normalization - SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(new ObjectMapper())); - assertNotNull(client); - - var httpClient = assertInstanceOf(Http2SolrClient.class, client); - assertEquals(expectedUrl, httpClient.getBaseURL()); - - // Clean up - try { - client.close(); - } catch (Exception e) { - // Ignore close errors in test - } - } - - @Test - void testUrlWithoutTrailingSlash() { - // Test URL without trailing slash branch - SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983"); - SolrConfig solrConfig = new SolrConfig(); - - SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(new ObjectMapper())); - Http2SolrClient httpClient = (Http2SolrClient) client; - - // Should add trailing slash and solr path - assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); - - try { - client.close(); - } catch (Exception e) { - // Ignore close errors in test - } - } - - @Test - void testUrlWithTrailingSlashButNoSolrPath() { - // Test URL with trailing slash but no solr path branch - SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/"); - SolrConfig solrConfig = new SolrConfig(); - - SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(new ObjectMapper())); - Http2SolrClient httpClient = (Http2SolrClient) client; - - // Should add solr path to existing trailing slash - assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); - - try { - client.close(); - } catch (Exception e) { - // Ignore close errors in test - } - } - - @Test - void testUrlWithSolrPathButNoTrailingSlash() { - // Test URL with solr path but no trailing slash - SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/solr"); - SolrConfig solrConfig = new SolrConfig(); - - SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(new ObjectMapper())); - Http2SolrClient httpClient = (Http2SolrClient) client; - - // Should add trailing slash - assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); - - try { - client.close(); - } catch (Exception e) { - // Ignore close errors in test - } - } - - @Test - void testUrlAlreadyProperlyFormatted() { - // Test URL that's already properly formatted - SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/solr/"); - SolrConfig solrConfig = new SolrConfig(); - - SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(new ObjectMapper())); - Http2SolrClient httpClient = (Http2SolrClient) client; - - // Should remain unchanged - assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); - - try { - client.close(); - } catch (Exception e) { - // Ignore close errors in test - } - } } diff --git a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java new file mode 100644 index 0000000..f90b449 --- /dev/null +++ b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.mcp.server.config; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.impl.Http2SolrClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; + +@JsonTest +class SolrConfigUrlNormalizationTest { + + @Autowired + private ObjectMapper objectMapper; + + @ParameterizedTest + @CsvSource({"http://localhost:8983, http://localhost:8983/solr", + "http://localhost:8983/, http://localhost:8983/solr", + "http://localhost:8983/solr, http://localhost:8983/solr", + "http://localhost:8983/solr/, http://localhost:8983/solr", + "http://localhost:8983/custom/solr/, http://localhost:8983/custom/solr"}) + void testUrlNormalization(String inputUrl, String expectedUrl) { + SolrConfigurationProperties testProperties = new SolrConfigurationProperties(inputUrl); + SolrConfig solrConfig = new SolrConfig(); + + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper)); + assertNotNull(client); + + var httpClient = assertInstanceOf(Http2SolrClient.class, client); + assertEquals(expectedUrl, httpClient.getBaseURL()); + + try { + client.close(); + } catch (Exception e) { + // Ignore close errors in test + } + } + + @Test + void testUrlWithoutTrailingSlash() { + SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983"); + SolrConfig solrConfig = new SolrConfig(); + + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper)); + Http2SolrClient httpClient = (Http2SolrClient) client; + + assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); + + try { + client.close(); + } catch (Exception e) { + // Ignore close errors in test + } + } + + @Test + void testUrlWithTrailingSlashButNoSolrPath() { + SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/"); + SolrConfig solrConfig = new SolrConfig(); + + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper)); + Http2SolrClient httpClient = (Http2SolrClient) client; + + assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); + + try { + client.close(); + } catch (Exception e) { + // Ignore close errors in test + } + } + + @Test + void testUrlWithSolrPathButNoTrailingSlash() { + SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/solr"); + SolrConfig solrConfig = new SolrConfig(); + + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper)); + Http2SolrClient httpClient = (Http2SolrClient) client; + + assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); + + try { + client.close(); + } catch (Exception e) { + // Ignore close errors in test + } + } + + @Test + void testUrlAlreadyProperlyFormatted() { + SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/solr/"); + SolrConfig solrConfig = new SolrConfig(); + + SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper)); + Http2SolrClient httpClient = (Http2SolrClient) client; + + assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); + + try { + client.close(); + } catch (Exception e) { + // Ignore close errors in test + } + } +} From 9224e8e2b14ae3f65461c4bf52f4cd60b1f1db4e Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Sun, 8 Mar 2026 17:07:30 -0400 Subject: [PATCH 5/8] fix(collection): catch RuntimeException from removed /admin/mbeans in Solr 10 SolrInfoMBeanHandler (and thus the /admin/mbeans endpoint) was removed in Solr 10. When getCacheMetrics() or getHandlerMetrics() call this endpoint on a Solr 10 server, SolrJ throws RemoteSolrException (a RuntimeException) because the server returns an HTML 404 page instead of JSON. Widen the catch in both methods to include RuntimeException so the server degrades gracefully (returning null for cache/handler stats) rather than propagating the exception. The integration tests already handle null stats, so all tests now pass with solr:10-slim. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: adityamparikh --- .../solr/mcp/server/metadata/CollectionService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java b/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java index 59a83ed..1cd65eb 100644 --- a/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java +++ b/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java @@ -609,7 +609,9 @@ public CacheStats getCacheMetrics(String collection) { } return stats; - } catch (SolrServerException | IOException e) { + } catch (SolrServerException | IOException | RuntimeException e) { + // RuntimeException covers SolrException subclasses (e.g. RemoteSolrException) + // thrown when the /admin/mbeans endpoint is unavailable (removed in Solr 10). return null; // Return null instead of empty object } } @@ -781,7 +783,9 @@ public HandlerStats getHandlerMetrics(String collection) { } return stats; - } catch (SolrServerException | IOException e) { + } catch (SolrServerException | IOException | RuntimeException e) { + // RuntimeException covers SolrException subclasses (e.g. RemoteSolrException) + // thrown when the /admin/mbeans endpoint is unavailable (removed in Solr 10). return null; // Return null instead of empty object } } From a75f28fa52d142e48a2d2aa3e543ba5e47449d49 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Fri, 6 Mar 2026 00:08:04 -0500 Subject: [PATCH 6/8] feat(ci): add Solr 9.10 and 10 compatibility testing Add Solr 9.10 and 10 to the CI compatibility matrix, running integration tests against all supported versions (8.11, 9.4, 9.9, 9.10, 10) on every PR and push to main. Also update AGENTS.md to document Solr 10 compatibility status: the /admin/mbeans endpoint removal is handled gracefully, and all other functionality is verified working with solr:10-slim. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: adityamparikh --- .github/workflows/build-and-publish.yml | 35 ++++++++++++++++++++++++- AGENTS.md | 20 +++++++++----- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index eacba70..27c3013 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -175,7 +175,40 @@ jobs: retention-days: 7 # ============================================================================ - # Job 2: Publish Docker Images + # Job 2: Solr Version Compatibility Tests + # ============================================================================ + # Tests the server against multiple Solr versions using Testcontainers. + # Runs in parallel so each Solr version is an independent job. + # Tested versions: 8.11, 9.4, 9.9, 9.10, 10 + # ============================================================================ + solr-compatibility: + name: Solr ${{ matrix.solr-version }} Compatibility + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + solr-version: + - "8.11-slim" + - "9.4-slim" + - "9.9-slim" + - "9.10-slim" + - "10-slim" + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java + uses: ./.github/actions/setup-java + + - name: Run tests against Solr ${{ matrix.solr-version }} + env: + SOLR_VERSION: ${{ matrix.solr-version }} + run: ./gradlew test "-Dsolr.test.image=solr:${SOLR_VERSION}" + + # ============================================================================ + # Job 3: Publish Docker Images # ============================================================================ # This job builds multi-platform Docker images using Jib and publishes them # to GitHub Container Registry (GHCR) and Docker Hub. diff --git a/AGENTS.md b/AGENTS.md index 57dd384..91f9d58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,18 +81,24 @@ The Solr Docker image used in tests is configurable via the `solr.test.image` sy ./gradlew test -Dsolr.test.image=solr:8.11-slim # Solr 8.11 ./gradlew test -Dsolr.test.image=solr:9.4-slim # Solr 9.4 ./gradlew test -Dsolr.test.image=solr:9.9-slim # Solr 9.9 (default) +./gradlew test -Dsolr.test.image=solr:9.10-slim # Solr 9.10 +./gradlew test -Dsolr.test.image=solr:10-slim # Solr 10 ``` -**Tested compatible versions:** 8.11, 9.4, 9.9 +**Tested compatible versions:** 8.11, 9.4, 9.9, 9.10, 10 -### Solr 10 Compatibility Notes +### Solr 10 Compatibility -Solr 10 introduces breaking changes that will require updates to this project: +Solr 10.0.0 is fully supported with the JSON wire format. The `/admin/mbeans` endpoint was +removed in Solr 10; `getCacheMetrics()` and `getHandlerMetrics()` now catch `RuntimeException` +(which covers `RemoteSolrException`) so they degrade gracefully and return `null`. Tests that +check `cacheStats` and `handlerStats` already handle `null` values. -- **MBeans removal:** `SolrInfoMBeanHandler` is removed. `CollectionService.getCollectionStats()` uses `/admin/mbeans` for cache and handler metrics — this will need to migrate to the `/admin/metrics` endpoint or OpenTelemetry. -- **Metrics migration:** Dropwizard metrics replaced by OpenTelemetry. All metric names switch to snake_case. JMX, Prometheus exporter, SLF4J, and Graphite reporters are removed. -- **SolrJ base URL:** SolrClient now only accepts root URLs (e.g., `http://host:8983/solr`). This project already uses root URLs with per-request collection names, so **no change needed** here. -- **SolrJ dependency:** Upgrade `solr-solrj` from 9.x to 10.x in `gradle/libs.versions.toml`. The Jetty BOM alignment (`jetty = "10.0.22"`) will also need updating since Solr 10 uses Jetty 12.x. +Remaining known differences from Solr 9: +- **`/admin/mbeans` removed:** Cache and handler stats from `getCollectionStats()` will always be `null` on Solr 10. A future migration to `/admin/metrics` will restore these metrics. +- **Metrics migration:** Dropwizard metrics replaced by OpenTelemetry. Metric names switch to snake_case in Solr 10. +- **SolrJ base URL:** Already uses root URLs — **no change needed**. +- **SolrJ 10.x dependency:** Not yet on Maven Central (as of 2026-03-06); tests use SolrJ 9.x against a Solr 10 server. Update `solr-solrj` and Jetty BOM when 10.x is released. ## Key Configuration From 96eb6c3dadcd2118da98e75432bf38fe2b3ec4fe Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Fri, 6 Mar 2026 00:14:57 -0500 Subject: [PATCH 7/8] feat(deps): upgrade solr-solrj from 9.9.0 to 10.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump solr version to 10.0.0 in libs.versions.toml - Remove Jetty BOM alignment (Solr 10 uses Jetty 12; no longer needed) - Remove Apache HttpComponents exclusion (SolrJ 10 no longer uses it) - Replace Http2SolrClient with HttpJdkSolrClient (new JDK HTTP client) - Move SolrQuery import: solrj → solrj.request package - Move ResponseParser import: solrj → solrj.response package - Adapt ResponseParser: getContentType() → getContentTypes() returning Collection; remove processResponse(Reader) (no longer abstract) - Fix CoreAdminResponse.getCoreStatus(): now returns Map instead of NamedList — update listCollections() and CollectionServiceTest accordingly All unit and Testcontainers integration tests pass. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: adityamparikh --- build.gradle.kts | 6 +----- gradle/libs.versions.toml | 4 +--- .../mcp/server/config/JsonResponseParser.java | 17 ++++------------- .../solr/mcp/server/config/SolrConfig.java | 8 ++++---- .../mcp/server/metadata/CollectionService.java | 9 ++------- .../solr/mcp/server/search/SearchService.java | 2 +- .../solr/mcp/server/config/SolrConfigTest.java | 6 +++--- .../config/SolrConfigUrlNormalizationTest.java | 12 ++++++------ .../server/metadata/CollectionServiceTest.java | 16 ++++++++-------- .../server/search/SearchServiceDirectTest.java | 2 +- .../mcp/server/search/SearchServiceTest.java | 2 +- 11 files changed, 32 insertions(+), 52 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 496f8cc..994fafb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -96,9 +96,7 @@ dependencies { implementation(libs.spring.boot.starter.web) implementation(libs.spring.boot.starter.actuator) implementation(libs.spring.ai.starter.mcp.server.webmvc) - implementation(libs.solr.solrj) { - exclude(group = "org.apache.httpcomponents") - } + implementation(libs.solr.solrj) implementation(libs.commons.csv) // JSpecify for nullability annotations implementation(libs.jspecify) @@ -119,8 +117,6 @@ dependencies { dependencyManagement { imports { mavenBom("org.springframework.ai:spring-ai-bom:${libs.versions.spring.ai.get()}") - // Align Jetty family to 10.x compatible with SolrJ 9.x - mavenBom("org.eclipse.jetty:jetty-bom:${libs.versions.jetty.get()}") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd71d2d..6244ce1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ spotless = "7.0.2" # Main dependencies spring-ai = "1.1.2" -solr = "9.9.0" +solr = "10.0.0" commons-csv = "1.10.0" jspecify = "1.0.0" mcp-server-security = "0.0.4" @@ -17,8 +17,6 @@ mcp-server-security = "0.0.4" errorprone-core = "2.38.0" nullaway = "0.12.7" -# Jetty BOM version -jetty = "10.0.22" # Test dependencies testcontainers = "1.21.3" diff --git a/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java b/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java index b57daf8..a78e3ab 100644 --- a/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java +++ b/src/main/java/org/apache/solr/mcp/server/config/JsonResponseParser.java @@ -20,10 +20,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; -import java.io.Reader; import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import org.apache.solr.client.solrj.ResponseParser; +import org.apache.solr.client.solrj.response.ResponseParser; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrException; @@ -80,8 +80,8 @@ public String getWriterType() { } @Override - public String getContentType() { - return MediaType.APPLICATION_JSON_VALUE; + public Collection getContentTypes() { + return List.of(MediaType.APPLICATION_JSON_VALUE); } @Override @@ -93,15 +93,6 @@ public NamedList processResponse(InputStream body, String encoding) { } } - @Override - public NamedList processResponse(Reader reader) { - try { - return toNamedList(mapper.readTree(reader)); - } catch (IOException e) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Failed to parse Solr JSON response", e); - } - } - private SimpleOrderedMap toNamedList(JsonNode objectNode) { SimpleOrderedMap result = new SimpleOrderedMap<>(); objectNode.fields().forEachRemaining(entry -> result.add(entry.getKey(), convertValue(entry.getValue()))); 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 ed2cf88..5a5d8d2 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 @@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.concurrent.TimeUnit; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.impl.Http2SolrClient; +import org.apache.solr.client.solrj.impl.HttpJdkSolrClient; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -96,7 +96,7 @@ * @version 1.0.0 * @since 1.0.0 * @see SolrConfigurationProperties - * @see Http2SolrClient + * @see HttpJdkSolrClient * @see org.springframework.boot.context.properties.EnableConfigurationProperties */ @Configuration @@ -165,7 +165,7 @@ public class SolrConfig { * the injected Solr configuration properties containing connection * URL * @return configured SolrClient instance ready for use in application services - * @see Http2SolrClient.Builder + * @see HttpJdkSolrClient.Builder * @see SolrConfigurationProperties#url() */ @Bean @@ -193,7 +193,7 @@ SolrClient solrClient(SolrConfigurationProperties properties, JsonResponseParser } // Use with explicit base URL; JSON wire format replaces the JavaBin default - return new Http2SolrClient.Builder(url).withConnectionTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) + return new HttpJdkSolrClient.Builder(url).withConnectionTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) .withIdleTimeout(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS).withResponseParser(jsonResponseParser) .build(); } diff --git a/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java b/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java index 1cd65eb..edad85a 100644 --- a/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java +++ b/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java @@ -27,7 +27,6 @@ import java.util.Date; import java.util.List; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CloudSolrClient; @@ -35,6 +34,7 @@ import org.apache.solr.client.solrj.request.CoreAdminRequest; import org.apache.solr.client.solrj.request.GenericSolrRequest; import org.apache.solr.client.solrj.request.LukeRequest; +import org.apache.solr.client.solrj.request.SolrQuery; import org.apache.solr.client.solrj.response.CollectionAdminResponse; import org.apache.solr.client.solrj.response.CoreAdminResponse; import org.apache.solr.client.solrj.response.LukeResponse; @@ -359,12 +359,7 @@ public List listCollections() { coreAdminRequest.setAction(CoreAdminParams.CoreAdminAction.STATUS); CoreAdminResponse coreResponse = coreAdminRequest.process(solrClient); - List cores = new ArrayList<>(); - NamedList> coreStatus = coreResponse.getCoreStatus(); - for (int i = 0; i < coreStatus.size(); i++) { - cores.add(coreStatus.getName(i)); - } - return cores; + return new ArrayList<>(coreResponse.getCoreStatus().keySet()); } } catch (SolrServerException | IOException e) { return new ArrayList<>(); diff --git a/src/main/java/org/apache/solr/mcp/server/search/SearchService.java b/src/main/java/org/apache/solr/mcp/server/search/SearchService.java index 31561db..9178ed2 100644 --- a/src/main/java/org/apache/solr/mcp/server/search/SearchService.java +++ b/src/main/java/org/apache/solr/mcp/server/search/SearchService.java @@ -22,8 +22,8 @@ import java.util.List; import java.util.Map; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.request.SolrQuery; import org.apache.solr.client.solrj.response.FacetField; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocument; diff --git a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java index ce109c6..44247fb 100644 --- a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java +++ b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java @@ -19,7 +19,7 @@ import static org.junit.jupiter.api.Assertions.*; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.impl.Http2SolrClient; +import org.apache.solr.client.solrj.impl.HttpJdkSolrClient; import org.apache.solr.mcp.server.TestcontainersConfiguration; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -49,9 +49,9 @@ void testSolrClientConfiguration() { // Verify that the SolrClient is using the correct URL // Note: SolrConfig normalizes the URL to have trailing slash, but - // Http2SolrClient removes + // HttpJdkSolrClient removes // it - var httpSolrClient = assertInstanceOf(Http2SolrClient.class, solrClient); + var httpSolrClient = assertInstanceOf(HttpJdkSolrClient.class, solrClient); String expectedUrl = "http://" + solrContainer.getHost() + ":" + solrContainer.getMappedPort(8983) + "/solr"; assertEquals(expectedUrl, httpSolrClient.getBaseURL()); } diff --git a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java index f90b449..01d99c8 100644 --- a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java @@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.impl.Http2SolrClient; +import org.apache.solr.client.solrj.impl.HttpJdkSolrClient; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -46,7 +46,7 @@ void testUrlNormalization(String inputUrl, String expectedUrl) { SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper)); assertNotNull(client); - var httpClient = assertInstanceOf(Http2SolrClient.class, client); + var httpClient = assertInstanceOf(HttpJdkSolrClient.class, client); assertEquals(expectedUrl, httpClient.getBaseURL()); try { @@ -62,7 +62,7 @@ void testUrlWithoutTrailingSlash() { SolrConfig solrConfig = new SolrConfig(); SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper)); - Http2SolrClient httpClient = (Http2SolrClient) client; + HttpJdkSolrClient httpClient = (HttpJdkSolrClient) client; assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); @@ -79,7 +79,7 @@ void testUrlWithTrailingSlashButNoSolrPath() { SolrConfig solrConfig = new SolrConfig(); SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper)); - Http2SolrClient httpClient = (Http2SolrClient) client; + HttpJdkSolrClient httpClient = (HttpJdkSolrClient) client; assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); @@ -96,7 +96,7 @@ void testUrlWithSolrPathButNoTrailingSlash() { SolrConfig solrConfig = new SolrConfig(); SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper)); - Http2SolrClient httpClient = (Http2SolrClient) client; + HttpJdkSolrClient httpClient = (HttpJdkSolrClient) client; assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); @@ -113,7 +113,7 @@ void testUrlAlreadyProperlyFormatted() { SolrConfig solrConfig = new SolrConfig(); SolrClient client = solrConfig.solrClient(testProperties, new JsonResponseParser(objectMapper)); - Http2SolrClient httpClient = (Http2SolrClient) client; + HttpJdkSolrClient httpClient = (HttpJdkSolrClient) client; assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); diff --git a/src/test/java/org/apache/solr/mcp/server/metadata/CollectionServiceTest.java b/src/test/java/org/apache/solr/mcp/server/metadata/CollectionServiceTest.java index 19888ae..26f5c03 100644 --- a/src/test/java/org/apache/solr/mcp/server/metadata/CollectionServiceTest.java +++ b/src/test/java/org/apache/solr/mcp/server/metadata/CollectionServiceTest.java @@ -26,7 +26,9 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; @@ -744,15 +746,13 @@ void listCollections_CloudClient_Error() throws Exception { @Test void listCollections_NonCloudClient_Success() throws Exception { - // Create a NamedList to represent the core status response + // Create a NamedList to represent the core status response. + // The "status" value must be a Map so SolrJ 10 can use Jackson convertValue + // to produce Map. NamedList response = new NamedList<>(); - NamedList status = new NamedList<>(); - - NamedList core1Status = new NamedList<>(); - NamedList core2Status = new NamedList<>(); - - status.add("core1", core1Status); - status.add("core2", core2Status); + Map status = new HashMap<>(); + status.put("core1", new HashMap<>()); + status.put("core2", new HashMap<>()); response.add("status", status); // Mock the solrClient request to return the response diff --git a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceDirectTest.java b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceDirectTest.java index 8358b9f..ae38775 100644 --- a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceDirectTest.java +++ b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceDirectTest.java @@ -26,8 +26,8 @@ import java.util.List; import java.util.Map; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.request.SolrQuery; import org.apache.solr.client.solrj.response.FacetField; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocument; 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 ab336bd..1e8f8be 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 @@ -28,9 +28,9 @@ import java.util.Map; import java.util.OptionalDouble; import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.SolrQuery; import org.apache.solr.client.solrj.response.FacetField; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocument; From 322efcfce1dee470f909f3d5f8fe59a6ba325f48 Mon Sep 17 00:00:00 2001 From: adityamparikh Date: Sun, 8 Mar 2026 20:09:17 -0400 Subject: [PATCH 8/8] fix(quality): resolve SonarQube violations - S1488: return stream expression directly in validateCollectionExists instead of assigning to local variable - S7467: replace unused caught exceptions with _ (Java 25 unnamed variables) in CollectionService and SolrConfigUrlNormalizationTest Signed-off-by: Aditya Parikh Co-Authored-By: Claude Opus 4.6 Signed-off-by: adityamparikh --- .../solr/mcp/server/metadata/CollectionService.java | 12 +++++------- .../config/SolrConfigUrlNormalizationTest.java | 10 +++++----- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java b/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java index edad85a..5b62644 100644 --- a/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java +++ b/src/main/java/org/apache/solr/mcp/server/metadata/CollectionService.java @@ -361,7 +361,7 @@ public List listCollections() { return new ArrayList<>(coreResponse.getCoreStatus().keySet()); } - } catch (SolrServerException | IOException e) { + } catch (SolrServerException | IOException _) { return new ArrayList<>(); } } @@ -604,7 +604,7 @@ public CacheStats getCacheMetrics(String collection) { } return stats; - } catch (SolrServerException | IOException | RuntimeException e) { + } catch (SolrServerException | IOException | RuntimeException _) { // RuntimeException covers SolrException subclasses (e.g. RemoteSolrException) // thrown when the /admin/mbeans endpoint is unavailable (removed in Solr 10). return null; // Return null instead of empty object @@ -778,7 +778,7 @@ public HandlerStats getHandlerMetrics(String collection) { } return stats; - } catch (SolrServerException | IOException | RuntimeException e) { + } catch (SolrServerException | IOException | RuntimeException _) { // RuntimeException covers SolrException subclasses (e.g. RemoteSolrException) // thrown when the /admin/mbeans endpoint is unavailable (removed in Solr 10). return null; // Return null instead of empty object @@ -964,10 +964,8 @@ private boolean validateCollectionExists(String collection) { // Check if any of the returned collections start with the collection name (for // shard // names) - boolean shardMatch = collections.stream().anyMatch(c -> c.startsWith(collection + SHARD_SUFFIX)); - - return shardMatch; - } catch (Exception e) { + return collections.stream().anyMatch(c -> c.startsWith(collection + SHARD_SUFFIX)); + } catch (Exception _) { return false; } } diff --git a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java index 01d99c8..3b4f8fc 100644 --- a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigUrlNormalizationTest.java @@ -51,7 +51,7 @@ void testUrlNormalization(String inputUrl, String expectedUrl) { try { client.close(); - } catch (Exception e) { + } catch (Exception _) { // Ignore close errors in test } } @@ -68,7 +68,7 @@ void testUrlWithoutTrailingSlash() { try { client.close(); - } catch (Exception e) { + } catch (Exception _) { // Ignore close errors in test } } @@ -85,7 +85,7 @@ void testUrlWithTrailingSlashButNoSolrPath() { try { client.close(); - } catch (Exception e) { + } catch (Exception _) { // Ignore close errors in test } } @@ -102,7 +102,7 @@ void testUrlWithSolrPathButNoTrailingSlash() { try { client.close(); - } catch (Exception e) { + } catch (Exception _) { // Ignore close errors in test } } @@ -119,7 +119,7 @@ void testUrlAlreadyProperlyFormatted() { try { client.close(); - } catch (Exception e) { + } catch (Exception _) { // Ignore close errors in test } }