diff --git a/build.gradle.kts b/build.gradle.kts index 96fd723..363308b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.spring.dependency.management) jacoco alias(libs.plugins.errorprone) + alias(libs.plugins.spotless) } group = "org.apache.solr" @@ -77,4 +78,24 @@ tasks.withType().configureEach { option("NullAway:OnlyNullMarked", "true") // Enable nullness checks only in null-marked code error("NullAway") // bump checks from warnings (default) to errors } -} \ No newline at end of file +} + +tasks.build { + dependsOn(tasks.spotlessApply) +} + +spotless { + java { + target("src/**/*.java") + googleJavaFormat().aosp().reflowLongStrings() + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + importOrder() + formatAnnotations() + } + kotlinGradle { + target("*.gradle.kts") + ktlint() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da81ac9..9de2429 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ spring-boot = "3.5.6" spring-dependency-management = "1.1.7" errorprone-plugin = "4.2.0" +spotless = "7.0.2" # Main dependencies spring-ai = "1.1.0-M3" @@ -79,4 +80,5 @@ errorprone = [ [plugins] spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" } -errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" } \ No newline at end of file +errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone-plugin" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } \ No newline at end of file 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 42ec12b..ed4c4d7 100644 --- a/src/main/java/org/apache/solr/mcp/server/Main.java +++ b/src/main/java/org/apache/solr/mcp/server/Main.java @@ -25,57 +25,65 @@ /** * Main Spring Boot application class for the Apache Solr Model Context Protocol (MCP) Server. - * - *

This class serves as the entry point for the Solr MCP Server application, which provides - * a bridge between AI clients (such as Claude Desktop) and Apache Solr search and indexing - * capabilities through the Model Context Protocol specification.

- * - *

Application Architecture:

+ * + *

This class serves as the entry point for the Solr MCP Server application, which provides a + * bridge between AI clients (such as Claude Desktop) and Apache Solr search and indexing + * capabilities through the Model Context Protocol specification. + * + *

Application Architecture: + * *

The application follows a service-oriented architecture where each major Solr operation - * category is encapsulated in its own service class:

+ * category is encapsulated in its own service class: + * * - * - *

Spring Boot Features:

+ * + *

Spring Boot Features: + * *

- * - *

Communication Flow:

+ * + *

Communication Flow: + * *

    - *
  1. AI client connects to MCP server via stdio
  2. - *
  3. Client discovers available tools through MCP protocol
  4. - *
  5. Client invokes tools with natural language parameters
  6. - *
  7. Server routes requests to appropriate service methods
  8. - *
  9. Services interact with Solr via SolrJ client library
  10. - *
  11. Results are serialized and returned to AI client
  12. + *
  13. AI client connects to MCP server via stdio + *
  14. Client discovers available tools through MCP protocol + *
  15. Client invokes tools with natural language parameters + *
  16. Server routes requests to appropriate service methods + *
  17. Services interact with Solr via SolrJ client library + *
  18. Results are serialized and returned to AI client *
- * - *

Configuration Requirements:

- *

The application requires the following configuration properties:

+ * + *

Configuration Requirements: + * + *

The application requires the following configuration properties: + * *

{@code
  * # application.properties
  * solr.url=http://localhost:8983
  * }
- * - *

Deployment Considerations:

+ * + *

Deployment Considerations: + * *

* * @version 0.0.1 * @since 0.0.1 - * * @see SearchService * @see IndexingService * @see CollectionService @@ -87,35 +95,36 @@ public class Main { /** * Main application entry point that starts the Spring Boot application. - * - *

This method initializes the Spring application context, configures all - * service beans, establishes Solr connectivity, and begins listening for - * MCP client connections via standard input/output.

- * - *

Startup Process:

+ * + *

This method initializes the Spring application context, configures all service beans, + * establishes Solr connectivity, and begins listening for MCP client connections via standard + * input/output. + * + *

Startup Process: + * *

    - *
  1. Initialize Spring Boot application context
  2. - *
  3. Load configuration properties from various sources
  4. - *
  5. Create and configure SolrClient bean
  6. - *
  7. Initialize all service beans with dependency injection
  8. - *
  9. Register MCP tools from service methods
  10. - *
  11. Start MCP server listening on stdio
  12. + *
  13. Initialize Spring Boot application context + *
  14. Load configuration properties from various sources + *
  15. Create and configure SolrClient bean + *
  16. Initialize all service beans with dependency injection + *
  17. Register MCP tools from service methods + *
  18. Start MCP server listening on stdio *
- * - *

Error Handling:

- *

Startup failures typically indicate configuration issues such as:

+ * + *

Error Handling: + * + *

Startup failures typically indicate configuration issues such as: + * *

- * + * * @param args command-line arguments passed to the application - * * @see SpringApplication#run(Class, String...) */ public static void main(String[] args) { SpringApplication.run(Main.class, args); } - -} \ No newline at end of file +} 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 610817e..d4e49cb 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,63 +16,67 @@ */ package org.apache.solr.mcp.server.config; +import java.util.concurrent.TimeUnit; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.impl.Http2SolrClient; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.util.concurrent.TimeUnit; - /** * Spring Configuration class for Apache Solr client setup and connection management. - * - *

This configuration class is responsible for creating and configuring the SolrJ client - * that serves as the primary interface for communication with Apache Solr servers. It handles - * URL normalization, connection parameters, and timeout configurations to ensure reliable - * connectivity for the MCP server operations.

- * - *

Configuration Features:

+ * + *

This configuration class is responsible for creating and configuring the SolrJ client that + * serves as the primary interface for communication with Apache Solr servers. It handles URL + * normalization, connection parameters, and timeout configurations to ensure reliable connectivity + * for the MCP server operations. + * + *

Configuration Features: + * *

- * - *

URL Processing:

- *

The configuration automatically normalizes Solr URLs to ensure proper communication:

+ * + *

URL Processing: + * + *

The configuration automatically normalizes Solr URLs to ensure proper communication: + * *

- * - *

Connection Parameters:

+ * + *

Connection Parameters: + * *

- * - *

Configuration Example:

+ * + *

Configuration Example: + * *

{@code
  * # application.properties
  * solr.url=http://localhost:8983
- * 
+ *
  * # Results in normalized URL: http://localhost:8983/solr/
  * }
- * - *

Supported URL Formats:

+ * + *

Supported URL Formats: + * *

* * @version 0.0.1 * @since 0.0.1 - * * @see SolrConfigurationProperties * @see Http2SolrClient * @see org.springframework.boot.context.properties.EnableConfigurationProperties @@ -87,44 +91,48 @@ public class SolrConfig { /** * Creates and configures a SolrClient bean for Apache Solr communication. - * - *

This method serves as the primary factory for creating SolrJ client instances - * that are used throughout the application for all Solr operations. It performs - * automatic URL normalization and applies production-ready timeout configurations.

- * - *

URL Normalization Process:

+ * + *

This method serves as the primary factory for creating SolrJ client instances that are + * used throughout the application for all Solr operations. It performs automatic URL + * normalization and applies production-ready timeout configurations. + * + *

URL Normalization Process: + * *

    - *
  1. Trailing Slash: Ensures URL ends with "/"
  2. - *
  3. Solr Path: Appends "/solr/" if not already present
  4. - *
  5. Validation: Checks for proper Solr endpoint format
  6. + *
  7. Trailing Slash: Ensures URL ends with "/" + *
  8. Solr Path: Appends "/solr/" if not already present + *
  9. Validation: Checks for proper Solr endpoint format *
- * - *

Connection Configuration:

+ * + *

Connection Configuration: + * *

- * - *

Client Type:

- *

Creates an {@code HttpSolrClient} configured for standard HTTP-based communication - * with Solr servers. This client type is suitable for both standalone Solr instances - * and SolrCloud deployments when used with load balancers.

- * - *

Error Handling:

- *

URL normalization is defensive and handles various input formats gracefully. - * Invalid URLs or connection failures will be caught during application startup - * or first usage, providing clear error messages for troubleshooting.

- * - *

Production Considerations:

+ * + *

Client Type: + * + *

Creates an {@code HttpSolrClient} configured for standard HTTP-based communication with + * Solr servers. This client type is suitable for both standalone Solr instances and SolrCloud + * deployments when used with load balancers. + * + *

Error Handling: + * + *

URL normalization is defensive and handles various input formats gracefully. Invalid URLs + * or connection failures will be caught during application startup or first usage, providing + * clear error messages for troubleshooting. + * + *

Production Considerations: + * *

- * + * * @param properties the injected Solr configuration properties containing connection URL * @return configured SolrClient instance ready for use in application services - * * @see Http2SolrClient.Builder * @see SolrConfigurationProperties#url() */ @@ -153,4 +161,4 @@ SolrClient solrClient(SolrConfigurationProperties properties) { .withIdleTimeout(SOCKET_TIMEOUT_MS, TimeUnit.MILLISECONDS) .build(); } -} \ No newline at end of file +} 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 20f2fb5..abbaf2f 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 @@ -20,76 +20,78 @@ /** * Spring Boot Configuration Properties record for Apache Solr connection settings. - * - *

This immutable configuration record encapsulates all external configuration - * properties required for establishing and maintaining connections to Apache Solr - * servers. It follows Spring Boot's type-safe configuration properties pattern - * using Java records for enhanced immutability and reduced boilerplate.

- * - *

Configuration Binding:

- *

This record automatically binds to configuration properties with the "solr" - * prefix from various configuration sources including:

+ * + *

This immutable configuration record encapsulates all external configuration properties + * required for establishing and maintaining connections to Apache Solr servers. It follows Spring + * Boot's type-safe configuration properties pattern using Java records for enhanced immutability + * and reduced boilerplate. + * + *

Configuration Binding: + * + *

This record automatically binds to configuration properties with the "solr" prefix from + * various configuration sources including: + * *

- * - *

Record Benefits:

+ * + *

Record Benefits: + * *

- * - *

URL Format Requirements:

- *

The Solr URL should point to the base Solr server endpoint. The configuration - * system will automatically normalize URLs to ensure proper formatting:

+ * + *

URL Format Requirements: + * + *

The Solr URL should point to the base Solr server endpoint. The configuration system will + * automatically normalize URLs to ensure proper formatting: + * *

- * - *

Environment-Specific Configuration:

+ * + *

Environment-Specific Configuration: + * *

{@code
  * # Development
  * solr.url=http://localhost:8983
- * 
- * # Staging  
+ *
+ * # Staging
  * solr.url=http://solr-staging.company.com:8983
- * 
+ *
  * # Production
  * solr.url=https://solr-prod.company.com:8983
  * }
- * - *

Integration with Dependency Injection:

- *

This record is automatically instantiated by Spring Boot's configuration - * properties mechanism and can be injected into any Spring-managed component - * that requires Solr connection information.

- * - *

Validation Considerations:

- *

While basic validation is handled by the configuration system, additional - * URL validation and normalization occurs in the {@link SolrConfig} class - * during SolrClient bean creation.

- * - * @param url the base URL of the Apache Solr server (required, non-null) * + *

Integration with Dependency Injection: + * + *

This record is automatically instantiated by Spring Boot's configuration properties mechanism + * and can be injected into any Spring-managed component that requires Solr connection information. + * + *

Validation Considerations: + * + *

While basic validation is handled by the configuration system, additional URL validation and + * normalization occurs in the {@link SolrConfig} class during SolrClient bean creation. + * + * @param url the base URL of the Apache Solr server (required, non-null) * @version 0.0.1 * @since 0.0.1 - * * @see SolrConfig * @see org.springframework.boot.context.properties.ConfigurationProperties * @see org.springframework.boot.context.properties.EnableConfigurationProperties */ @ConfigurationProperties(prefix = "solr") -public record SolrConfigurationProperties(String url) { - -} \ No newline at end of file +public record SolrConfigurationProperties(String url) {} diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/IndexingService.java b/src/main/java/org/apache/solr/mcp/server/indexing/IndexingService.java index 5882955..c11b1d3 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/IndexingService.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/IndexingService.java @@ -16,6 +16,9 @@ */ package org.apache.solr.mcp.server.indexing; +import java.io.IOException; +import java.util.List; +import javax.xml.parsers.ParserConfigurationException; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.common.SolrInputDocument; @@ -25,51 +28,54 @@ import org.springframework.stereotype.Service; import org.xml.sax.SAXException; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; -import java.util.List; - /** * Spring Service providing comprehensive document indexing capabilities for Apache Solr collections * through Model Context Protocol (MCP) integration. * - *

This service handles the conversion of JSON, CSV, and XML documents into Solr-compatible format and manages - * the indexing process with robust error handling and batch processing capabilities. It employs a - * schema-less approach where Solr automatically detects field types, eliminating the need for - * predefined schema configuration.

- * - *

Core Features:

+ *

This service handles the conversion of JSON, CSV, and XML documents into Solr-compatible + * format and manages the indexing process with robust error handling and batch processing + * capabilities. It employs a schema-less approach where Solr automatically detects field types, + * eliminating the need for predefined schema configuration. + * + *

Core Features: + * *

- * - *

MCP Tool Integration:

+ * + *

MCP Tool Integration: + * *

The service exposes indexing functionality as MCP tools that can be invoked by AI clients * through natural language requests. This enables seamless document ingestion workflows from - * external data sources.

- * - *

JSON Document Processing:

+ * external data sources. + * + *

JSON Document Processing: + * *

The service processes JSON documents by flattening nested objects using underscore notation * (e.g., "user.name" becomes "user_name") and handles arrays by converting them to multi-valued - * fields that Solr natively supports.

- * - *

Batch Processing Strategy:

+ * fields that Solr natively supports. + * + *

Batch Processing Strategy: + * *

Uses configurable batch sizes (default 1000 documents) for optimal performance. If a batch - * fails, the service automatically retries by indexing documents individually to identify and - * skip problematic documents while preserving valid ones.

- * - *

Example Usage:

+ * fails, the service automatically retries by indexing documents individually to identify and skip + * problematic documents while preserving valid ones. + * + *

Example Usage: + * *

{@code
  * // Index JSON array of documents
  * String jsonData = "[{\"title\":\"Document 1\",\"content\":\"Content here\"}]";
  * indexingService.indexDocuments("my_collection", jsonData);
- * 
+ *
  * // Programmatic document creation and indexing
  * List docs = indexingService.createSchemalessDocuments(jsonData);
  * int successful = indexingService.indexDocuments("my_collection", docs);
@@ -77,7 +83,6 @@
  *
  * @version 0.0.1
  * @since 0.0.1
- * 
  * @see SolrInputDocument
  * @see SolrClient
  * @see org.springframework.ai.tool.annotation.Tool
@@ -90,160 +95,173 @@ public class IndexingService {
     /** SolrJ client for communicating with Solr server */
     private final SolrClient solrClient;
 
-    /**
-     * Service for creating SolrInputDocument objects from various data formats
-     */
+    /** Service for creating SolrInputDocument objects from various data formats */
     private final IndexingDocumentCreator indexingDocumentCreator;
 
     /**
      * Constructs a new IndexingService with the required dependencies.
-     * 
-     * 

This constructor is automatically called by Spring's dependency injection - * framework during application startup, providing the service with the necessary - * Solr client and configuration components.

- * - * @param solrClient the SolrJ client instance for communicating with Solr * + *

This constructor is automatically called by Spring's dependency injection framework during + * application startup, providing the service with the necessary Solr client and configuration + * components. + * + * @param solrClient the SolrJ client instance for communicating with Solr * @see SolrClient */ - public IndexingService(SolrClient solrClient, - IndexingDocumentCreator indexingDocumentCreator) { + public IndexingService(SolrClient solrClient, IndexingDocumentCreator indexingDocumentCreator) { this.solrClient = solrClient; this.indexingDocumentCreator = indexingDocumentCreator; } /** * Indexes documents from a JSON string into a specified Solr collection. - * - *

This method serves as the primary entry point for document indexing operations - * and is exposed as an MCP tool for AI client interactions. It processes JSON data - * containing document arrays and indexes them using a schema-less approach.

- * - *

Supported JSON Formats:

+ * + *

This method serves as the primary entry point for document indexing operations and is + * exposed as an MCP tool for AI client interactions. It processes JSON data containing document + * arrays and indexes them using a schema-less approach. + * + *

Supported JSON Formats: + * *

    - *
  • Document Array: {@code [{"field1":"value1"},{"field2":"value2"}]}
  • - *
  • Nested Objects: Automatically flattened with underscore notation
  • - *
  • Multi-valued Fields: Arrays converted to Solr multi-valued fields
  • + *
  • Document Array: {@code [{"field1":"value1"},{"field2":"value2"}]} + *
  • Nested Objects: Automatically flattened with underscore notation + *
  • Multi-valued Fields: Arrays converted to Solr multi-valued fields *
- * - *

Processing Workflow:

+ * + *

Processing Workflow: + * *

    - *
  1. Parse JSON string into structured documents
  2. - *
  3. Convert to schema-less SolrInputDocument objects
  4. - *
  5. Execute batch indexing with error handling
  6. - *
  7. Commit changes to make documents searchable
  8. + *
  9. Parse JSON string into structured documents + *
  10. Convert to schema-less SolrInputDocument objects + *
  11. Execute batch indexing with error handling + *
  12. Commit changes to make documents searchable *
- * - *

MCP Tool Usage:

- *

AI clients can invoke this method with natural language requests like - * "index these documents into my_collection" or "add this JSON data to the search index".

- * - *

Error Handling:

- *

If indexing fails, the method attempts individual document processing to maximize - * the number of successfully indexed documents. Detailed error information is logged - * for troubleshooting purposes.

- * + * + *

MCP Tool Usage: + * + *

AI clients can invoke this method with natural language requests like "index these + * documents into my_collection" or "add this JSON data to the search index". + * + *

Error Handling: + * + *

If indexing fails, the method attempts individual document processing to maximize the + * number of successfully indexed documents. Detailed error information is logged for + * troubleshooting purposes. + * * @param collection the name of the Solr collection to index documents into * @param json JSON string containing an array of documents to index - * * @throws IOException if there are critical errors in JSON parsing or Solr communication * @throws SolrServerException if Solr server encounters errors during indexing - * * @see IndexingDocumentCreator#createSchemalessDocumentsFromJson(String) * @see #indexDocuments(String, List) */ - @McpTool(name = "index_json_documents", description = "Index documents from json String into Solr collection") + @McpTool( + name = "index_json_documents", + description = "Index documents from json String into Solr collection") public void indexJsonDocuments( @McpToolParam(description = "Solr collection to index into") String collection, - @McpToolParam(description = "JSON string containing documents to index") String json) throws IOException, SolrServerException { - List schemalessDoc = indexingDocumentCreator.createSchemalessDocumentsFromJson(json); + @McpToolParam(description = "JSON string containing documents to index") String json) + throws IOException, SolrServerException { + List schemalessDoc = + indexingDocumentCreator.createSchemalessDocumentsFromJson(json); indexDocuments(collection, schemalessDoc); } - /** * Indexes documents from a CSV string into a specified Solr collection. - * - *

This method serves as the primary entry point for CSV document indexing operations - * and is exposed as an MCP tool for AI client interactions. It processes CSV data - * with headers and indexes them using a schema-less approach.

- * - *

Supported CSV Formats:

+ * + *

This method serves as the primary entry point for CSV document indexing operations and is + * exposed as an MCP tool for AI client interactions. It processes CSV data with headers and + * indexes them using a schema-less approach. + * + *

Supported CSV Formats: + * *

    - *
  • Header Row Required: First row must contain column names
  • - *
  • Comma Delimited: Standard CSV format with comma separators
  • - *
  • Mixed Data Types: Automatic type detection by Solr
  • + *
  • Header Row Required: First row must contain column names + *
  • Comma Delimited: Standard CSV format with comma separators + *
  • Mixed Data Types: Automatic type detection by Solr *
- * - *

Processing Workflow:

+ * + *

Processing Workflow: + * *

    - *
  1. Parse CSV string to extract headers and data rows
  2. - *
  3. Convert to schema-less SolrInputDocument objects
  4. - *
  5. Execute batch indexing with error handling
  6. - *
  7. Commit changes to make documents searchable
  8. + *
  9. Parse CSV string to extract headers and data rows + *
  10. Convert to schema-less SolrInputDocument objects + *
  11. Execute batch indexing with error handling + *
  12. Commit changes to make documents searchable *
- * - *

MCP Tool Usage:

- *

AI clients can invoke this method with natural language requests like - * "index this CSV data into my_collection" or "add these CSV records to the search index".

- * - *

Error Handling:

- *

If indexing fails, the method attempts individual document processing to maximize - * the number of successfully indexed documents. Detailed error information is logged - * for troubleshooting purposes.

- * + * + *

MCP Tool Usage: + * + *

AI clients can invoke this method with natural language requests like "index this CSV data + * into my_collection" or "add these CSV records to the search index". + * + *

Error Handling: + * + *

If indexing fails, the method attempts individual document processing to maximize the + * number of successfully indexed documents. Detailed error information is logged for + * troubleshooting purposes. + * * @param collection the name of the Solr collection to index documents into * @param csv CSV string containing documents to index (first row must be headers) - * * @throws IOException if there are critical errors in CSV parsing or Solr communication * @throws SolrServerException if Solr server encounters errors during indexing - * * @see IndexingDocumentCreator#createSchemalessDocumentsFromCsv(String) * @see #indexDocuments(String, List) */ - @McpTool(name = "index_csv_documents", description = "Index documents from CSV string into Solr collection") + @McpTool( + name = "index_csv_documents", + description = "Index documents from CSV string into Solr collection") public void indexCsvDocuments( @McpToolParam(description = "Solr collection to index into") String collection, - @McpToolParam(description = "CSV string containing documents to index") String csv) throws IOException, SolrServerException { - List schemalessDoc = indexingDocumentCreator.createSchemalessDocumentsFromCsv(csv); + @McpToolParam(description = "CSV string containing documents to index") String csv) + throws IOException, SolrServerException { + List schemalessDoc = + indexingDocumentCreator.createSchemalessDocumentsFromCsv(csv); indexDocuments(collection, schemalessDoc); } /** * Indexes documents from an XML string into a specified Solr collection. * - *

This method serves as the primary entry point for XML document indexing operations - * and is exposed as an MCP tool for AI client interactions. It processes XML data - * with nested elements and attributes, indexing them using a schema-less approach.

+ *

This method serves as the primary entry point for XML document indexing operations and is + * exposed as an MCP tool for AI client interactions. It processes XML data with nested elements + * and attributes, indexing them using a schema-less approach. + * + *

Supported XML Formats: * - *

Supported XML Formats:

*
    - *
  • Single Document: Root element treated as one document
  • - *
  • Multiple Documents: Child elements with 'doc', 'item', or 'record' names treated as separate documents
  • - *
  • Nested Elements: Automatically flattened with underscore notation
  • - *
  • Attributes: Converted to fields with "_attr" suffix
  • - *
  • Mixed Data Types: Automatic type detection by Solr
  • + *
  • Single Document: Root element treated as one document + *
  • Multiple Documents: Child elements with 'doc', 'item', or 'record' + * names treated as separate documents + *
  • Nested Elements: Automatically flattened with underscore notation + *
  • Attributes: Converted to fields with "_attr" suffix + *
  • Mixed Data Types: Automatic type detection by Solr *
* - *

Processing Workflow:

+ *

Processing Workflow: + * *

    - *
  1. Parse XML string to extract elements and attributes
  2. - *
  3. Flatten nested structures using underscore notation
  4. - *
  5. Convert to schema-less SolrInputDocument objects
  6. - *
  7. Execute batch indexing with error handling
  8. - *
  9. Commit changes to make documents searchable
  10. + *
  11. Parse XML string to extract elements and attributes + *
  12. Flatten nested structures using underscore notation + *
  13. Convert to schema-less SolrInputDocument objects + *
  14. Execute batch indexing with error handling + *
  15. Commit changes to make documents searchable *
* - *

MCP Tool Usage:

- *

AI clients can invoke this method with natural language requests like - * "index this XML data into my_collection" or "add these XML records to the search index".

+ *

MCP Tool Usage: + * + *

AI clients can invoke this method with natural language requests like "index this XML data + * into my_collection" or "add these XML records to the search index". + * + *

Error Handling: + * + *

If indexing fails, the method attempts individual document processing to maximize the + * number of successfully indexed documents. Detailed error information is logged for + * troubleshooting purposes. * - *

Error Handling:

- *

If indexing fails, the method attempts individual document processing to maximize - * the number of successfully indexed documents. Detailed error information is logged - * for troubleshooting purposes.

+ *

Example XML Processing: * - *

Example XML Processing:

*
{@code
      * Input:
      * 
@@ -259,7 +277,7 @@ public void indexCsvDocuments(
      * }
* * @param collection the name of the Solr collection to index documents into - * @param xml XML string containing documents to index + * @param xml XML string containing documents to index * @throws ParserConfigurationException if XML parser configuration fails * @throws SAXException if XML parsing fails due to malformed content * @throws IOException if I/O errors occur during parsing or Solr communication @@ -267,60 +285,67 @@ public void indexCsvDocuments( * @see IndexingDocumentCreator#createSchemalessDocumentsFromXml(String) * @see #indexDocuments(String, List) */ - @McpTool(name = "index_xml_documents", description = "Index documents from XML string into Solr collection") + @McpTool( + name = "index_xml_documents", + description = "Index documents from XML string into Solr collection") public void indexXmlDocuments( @McpToolParam(description = "Solr collection to index into") String collection, - @McpToolParam(description = "XML string containing documents to index") String xml) throws ParserConfigurationException, SAXException, IOException, SolrServerException { - List schemalessDoc = indexingDocumentCreator.createSchemalessDocumentsFromXml(xml); + @McpToolParam(description = "XML string containing documents to index") String xml) + throws ParserConfigurationException, SAXException, IOException, SolrServerException { + List schemalessDoc = + indexingDocumentCreator.createSchemalessDocumentsFromXml(xml); indexDocuments(collection, schemalessDoc); } /** * Indexes a list of SolrInputDocument objects into a Solr collection using batch processing. - * - *

This method implements a robust batch indexing strategy that optimizes performance - * while providing resilience against individual document failures. It processes documents - * in configurable batches and includes fallback mechanisms for error recovery.

- * - *

Batch Processing Strategy:

+ * + *

This method implements a robust batch indexing strategy that optimizes performance while + * providing resilience against individual document failures. It processes documents in + * configurable batches and includes fallback mechanisms for error recovery. + * + *

Batch Processing Strategy: + * *

    - *
  • Batch Size: Configurable (default 1000) for optimal performance
  • - *
  • Error Recovery: Individual document retry on batch failure
  • - *
  • Success Tracking: Accurate count of successfully indexed documents
  • - *
  • Commit Strategy: Single commit after all batches for consistency
  • + *
  • Batch Size: Configurable (default 1000) for optimal performance + *
  • Error Recovery: Individual document retry on batch failure + *
  • Success Tracking: Accurate count of successfully indexed documents + *
  • Commit Strategy: Single commit after all batches for consistency *
- * - *

Error Handling Workflow:

+ * + *

Error Handling Workflow: + * *

    - *
  1. Attempt batch indexing for optimal performance
  2. - *
  3. On batch failure, retry each document individually
  4. - *
  5. Track successful vs failed document counts
  6. - *
  7. Continue processing remaining batches despite failures
  8. - *
  9. Commit all successful changes at the end
  10. + *
  11. Attempt batch indexing for optimal performance + *
  12. On batch failure, retry each document individually + *
  13. Track successful vs failed document counts + *
  14. Continue processing remaining batches despite failures + *
  15. Commit all successful changes at the end *
- * - *

Performance Considerations:

+ * + *

Performance Considerations: + * *

Batch processing significantly improves indexing performance compared to individual - * document operations. The fallback to individual processing ensures maximum document - * ingestion even when some documents have issues.

- * - *

Transaction Behavior:

+ * document operations. The fallback to individual processing ensures maximum document ingestion + * even when some documents have issues. + * + *

Transaction Behavior: + * *

The method commits changes after all batches are processed, making indexed documents * immediately searchable. This ensures atomicity at the operation level while maintaining - * performance through batching.

- * + * performance through batching. + * * @param collection the name of the Solr collection to index into * @param documents list of SolrInputDocument objects to index * @return the number of documents successfully indexed - * * @throws SolrServerException if there are critical errors in Solr communication * @throws IOException if there are critical errors in commit operations - * * @see SolrInputDocument * @see SolrClient#add(String, java.util.Collection) * @see SolrClient#commit(String) */ - public int indexDocuments(String collection, List documents) throws SolrServerException, IOException { + public int indexDocuments(String collection, List documents) + throws SolrServerException, IOException { int successCount = 0; final int batchSize = DEFAULT_BATCH_SIZE; @@ -338,7 +363,8 @@ public int indexDocuments(String collection, List documents) solrClient.add(collection, doc); successCount++; } catch (SolrServerException | IOException | RuntimeException docError) { - // Document failed to index - this is expected behavior for problematic documents + // Document failed to index - this is expected behavior for problematic + // documents // We continue processing the rest of the batch } } @@ -348,5 +374,4 @@ public int indexDocuments(String collection, List documents) solrClient.commit(collection); return successCount; } - -} \ No newline at end of file +} diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java index 0119fe1..d1f84ea 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java @@ -16,23 +16,22 @@ */ package org.apache.solr.mcp.server.indexing.documentcreator; -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVParser; -import org.apache.commons.csv.CSVRecord; -import org.apache.solr.common.SolrInputDocument; -import org.springframework.stereotype.Component; - import java.io.IOException; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; +import org.apache.solr.common.SolrInputDocument; +import org.springframework.stereotype.Component; /** * Utility class for processing CSV documents and converting them to SolrInputDocument objects. * - *

This class handles the conversion of CSV documents into Solr-compatible format - * using a schema-less approach where Solr automatically detects field types.

+ *

This class handles the conversion of CSV documents into Solr-compatible format using a + * schema-less approach where Solr automatically detects field types. */ @Component public class CsvDocumentCreator implements SolrDocumentCreator { @@ -42,32 +41,37 @@ public class CsvDocumentCreator implements SolrDocumentCreator { /** * Creates a list of schema-less SolrInputDocument objects from a CSV string. * - *

This method implements a flexible document conversion strategy that allows Solr - * to automatically detect field types without requiring predefined schema configuration. - * It processes CSV data by using the first row as field headers and converting each - * subsequent row into a document.

+ *

This method implements a flexible document conversion strategy that allows Solr to + * automatically detect field types without requiring predefined schema configuration. It + * processes CSV data by using the first row as field headers and converting each subsequent row + * into a document. + * + *

Schema-less Benefits: * - *

Schema-less Benefits:

*
    - *
  • Flexibility: No need to predefine field types in schema
  • - *
  • Rapid Prototyping: Quick iteration on document structures
  • - *
  • Type Detection: Solr automatically infers optimal field types
  • - *
  • Dynamic Fields: Support for varying document structures
  • + *
  • Flexibility: No need to predefine field types in schema + *
  • Rapid Prototyping: Quick iteration on document structures + *
  • Type Detection: Solr automatically infers optimal field types + *
  • Dynamic Fields: Support for varying document structures *
* - *

CSV Processing Rules:

+ *

CSV Processing Rules: + * *

    - *
  • Header Row: First row defines field names, automatically sanitized
  • - *
  • Empty Values: Ignored and not indexed
  • - *
  • Type Detection: Solr handles numeric, boolean, and string types automatically
  • - *
  • Field Sanitization: Column names cleaned for Solr compatibility
  • + *
  • Header Row: First row defines field names, automatically sanitized + *
  • Empty Values: Ignored and not indexed + *
  • Type Detection: Solr handles numeric, boolean, and string types + * automatically + *
  • Field Sanitization: Column names cleaned for Solr compatibility *
* - *

Field Name Sanitization:

- *

Field names are automatically sanitized to ensure Solr compatibility by removing - * special characters and converting to lowercase with underscore separators.

+ *

Field Name Sanitization: + * + *

Field names are automatically sanitized to ensure Solr compatibility by removing special + * characters and converting to lowercase with underscore separators. + * + *

Example Transformation: * - *

Example Transformation:

*
{@code
      * Input CSV:
      * id,name,price,inStock
@@ -79,19 +83,23 @@ public class CsvDocumentCreator implements SolrDocumentCreator {
      *
      * @param csv CSV string containing document data (first row must be headers)
      * @return list of SolrInputDocument objects ready for indexing
-     * @throws DocumentProcessingException if CSV parsing fails, input validation fails, or the structure is invalid
+     * @throws DocumentProcessingException if CSV parsing fails, input validation fails, or the
+     *     structure is invalid
      * @see SolrInputDocument
      * @see FieldNameSanitizer#sanitizeFieldName(String)
      */
     public List create(String csv) throws DocumentProcessingException {
         if (csv.getBytes(StandardCharsets.UTF_8).length > MAX_INPUT_SIZE_BYTES) {
-            throw new DocumentProcessingException("Input too large: exceeds maximum size of " + MAX_INPUT_SIZE_BYTES + " bytes");
+            throw new DocumentProcessingException(
+                    "Input too large: exceeds maximum size of " + MAX_INPUT_SIZE_BYTES + " bytes");
         }
 
         List documents = new ArrayList<>();
 
-        try (CSVParser parser = new CSVParser(new StringReader(csv),
-                CSVFormat.Builder.create().setHeader().setTrim(true).build())) {
+        try (CSVParser parser =
+                new CSVParser(
+                        new StringReader(csv),
+                        CSVFormat.Builder.create().setHeader().setTrim(true).build())) {
             List headers = new ArrayList<>(parser.getHeaderNames());
             headers.replaceAll(FieldNameSanitizer::sanitizeFieldName);
 
@@ -117,5 +125,4 @@ public List create(String csv) throws DocumentProcessingExcep
 
         return documents;
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/DocumentProcessingException.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/DocumentProcessingException.java
index 850b26f..6d0c22f 100644
--- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/DocumentProcessingException.java
+++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/DocumentProcessingException.java
@@ -20,15 +20,16 @@
  * Exception thrown when document processing operations fail.
  *
  * 

This exception provides a unified error handling mechanism for all document creator - * implementations, wrapping various underlying exceptions while preserving the original - * error context and stack trace information.

+ * implementations, wrapping various underlying exceptions while preserving the original error + * context and stack trace information. + * + *

Common scenarios where this exception is thrown: * - *

Common scenarios where this exception is thrown:

*
    - *
  • Invalid document format or structure
  • - *
  • Document parsing errors (JSON, XML, CSV)
  • - *
  • Input validation failures
  • - *
  • Resource access or I/O errors during processing
  • + *
  • Invalid document format or structure + *
  • Document parsing errors (JSON, XML, CSV) + *
  • Input validation failures + *
  • Resource access or I/O errors during processing *
*/ public class DocumentProcessingException extends RuntimeException { @@ -45,11 +46,11 @@ public DocumentProcessingException(String message) { /** * Constructs a new DocumentProcessingException with the specified detail message and cause. * - *

This constructor is particularly useful for wrapping underlying exceptions - * while providing additional context about the document processing failure.

+ *

This constructor is particularly useful for wrapping underlying exceptions while providing + * additional context about the document processing failure. * * @param message the detail message explaining the error - * @param cause the cause of this exception (which is saved for later retrieval) + * @param cause the cause of this exception (which is saved for later retrieval) */ public DocumentProcessingException(String message, Throwable cause) { super(message, cause); @@ -58,11 +59,11 @@ public DocumentProcessingException(String message, Throwable cause) { /** * Constructs a new DocumentProcessingException with the specified cause. * - *

The detail message is automatically derived from the cause's toString() method.

+ *

The detail message is automatically derived from the cause's toString() method. * * @param cause the cause of this exception (which is saved for later retrieval) */ public DocumentProcessingException(Throwable cause) { super(cause); } -} \ No newline at end of file +} diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java index 6a51c5a..9879d8c 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java @@ -19,32 +19,34 @@ import java.util.regex.Pattern; /** - * Utility class for sanitizing field names to ensure compatibility with Solr's field naming requirements. + * Utility class for sanitizing field names to ensure compatibility with Solr's field naming + * requirements. * - *

This class provides shared regex patterns and sanitization logic that can be used across - * all document creators to ensure consistent field name handling.

+ *

This class provides shared regex patterns and sanitization logic that can be used across all + * document creators to ensure consistent field name handling. * - *

Solr has specific requirements for field names that must be met to ensure proper - * indexing and searching functionality. This utility transforms arbitrary field names - * into Solr-compliant identifiers.

+ *

Solr has specific requirements for field names that must be met to ensure proper indexing and + * searching functionality. This utility transforms arbitrary field names into Solr-compliant + * identifiers. */ public final class FieldNameSanitizer { /** - * Pattern to match invalid characters in field names. - * Matches any character that is not alphanumeric or underscore. + * Pattern to match invalid characters in field names. Matches any character that is not + * alphanumeric or underscore. */ private static final Pattern INVALID_CHARACTERS_PATTERN = Pattern.compile("[\\W]"); /** - * Pattern to match leading and trailing underscores. - * Uses explicit grouping to make operator precedence clear. + * Pattern to match leading and trailing underscores. Uses explicit grouping to make operator + * precedence clear. */ - private static final Pattern LEADING_TRAILING_UNDERSCORES_PATTERN = Pattern.compile("(^_+)|(_+$)"); + private static final Pattern LEADING_TRAILING_UNDERSCORES_PATTERN = + Pattern.compile("(^_+)|(_+$)"); /** - * Pattern to match multiple consecutive underscores. - * Matches two or more consecutive underscores to collapse them into one. + * Pattern to match multiple consecutive underscores. Matches two or more consecutive + * underscores to collapse them into one. */ private static final Pattern MULTIPLE_UNDERSCORES_PATTERN = Pattern.compile("_{2,}"); @@ -56,32 +58,39 @@ private FieldNameSanitizer() { /** * Sanitizes field names to ensure they are compatible with Solr's field naming requirements. * - *

Sanitization Rules:

+ *

Sanitization Rules: + * *

    - *
  • Case Conversion: All characters converted to lowercase
  • - *
  • Character Replacement: Non-alphanumeric characters replaced with underscores
  • - *
  • Edge Trimming: Leading and trailing underscores removed
  • - *
  • Duplicate Compression: Multiple consecutive underscores collapsed to single
  • - *
  • Numeric Prefix: Field names starting with numbers get "field_" prefix
  • + *
  • Case Conversion: All characters converted to lowercase + *
  • Character Replacement: Non-alphanumeric characters replaced with + * underscores + *
  • Edge Trimming: Leading and trailing underscores removed + *
  • Duplicate Compression: Multiple consecutive underscores collapsed to + * single + *
  • Numeric Prefix: Field names starting with numbers get "field_" prefix *
* - *

Example Transformations:

+ *

Example Transformations: + * *

    - *
  • "User-Name" → "user_name"
  • - *
  • "product.price" → "product_price"
  • - *
  • "__field__name__" → "field_name"
  • - *
  • "Field123@Test" → "field123_test"
  • - *
  • "123field" → "field_123field"
  • + *
  • "User-Name" → "user_name" + *
  • "product.price" → "product_price" + *
  • "__field__name__" → "field_name" + *
  • "Field123@Test" → "field123_test" + *
  • "123field" → "field_123field" *
* * @param fieldName the original field name to sanitize - * @return sanitized field name compatible with Solr requirements, or "field" if input is null/empty - * @see Solr Field Guide + * @return sanitized field name compatible with Solr requirements, or "field" if input is + * null/empty + * @see Solr + * Field Guide */ public static String sanitizeFieldName(String fieldName) { // Convert to lowercase and replace invalid characters with underscores - String sanitized = INVALID_CHARACTERS_PATTERN.matcher(fieldName.toLowerCase()).replaceAll("_"); + String sanitized = + INVALID_CHARACTERS_PATTERN.matcher(fieldName.toLowerCase()).replaceAll("_"); // Remove leading/trailing underscores and collapse multiple underscores sanitized = LEADING_TRAILING_UNDERSCORES_PATTERN.matcher(sanitized).replaceAll(""); @@ -99,4 +108,4 @@ public static String sanitizeFieldName(String fieldName) { return sanitized; } -} \ No newline at end of file +} diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/IndexingDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/IndexingDocumentCreator.java index 7d16a0c..5c9e595 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/IndexingDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/IndexingDocumentCreator.java @@ -16,27 +16,29 @@ */ package org.apache.solr.mcp.server.indexing.documentcreator; +import java.nio.charset.StandardCharsets; +import java.util.List; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.mcp.server.indexing.IndexingService; import org.springframework.stereotype.Service; -import java.nio.charset.StandardCharsets; -import java.util.List; - /** * Spring Service responsible for creating SolrInputDocument objects from various data formats. * - *

This service handles the conversion of JSON, CSV, and XML documents into Solr-compatible format - * using a schema-less approach where Solr automatically detects field types, eliminating the need for - * predefined schema configuration.

+ *

This service handles the conversion of JSON, CSV, and XML documents into Solr-compatible + * format using a schema-less approach where Solr automatically detects field types, eliminating the + * need for predefined schema configuration. + * + *

Core Features: * - *

Core Features:

*
    - *
  • Schema-less Document Creation: Automatic field type detection by Solr
  • - *
  • JSON Processing: Support for complex nested JSON documents
  • - *
  • CSV Processing: Support for comma-separated value files with headers
  • - *
  • XML Processing: Support for XML documents with element flattening and attribute handling
  • - *
  • Field Sanitization: Automatic cleanup of field names for Solr compatibility
  • + *
  • Schema-less Document Creation: Automatic field type detection by Solr + *
  • JSON Processing: Support for complex nested JSON documents + *
  • CSV Processing: Support for comma-separated value files with headers + *
  • XML Processing: Support for XML documents with element flattening and + * attribute handling + *
  • Field Sanitization: Automatic cleanup of field names for Solr + * compatibility *
* * @version 0.0.1 @@ -55,9 +57,10 @@ public class IndexingDocumentCreator { private final JsonDocumentCreator jsonDocumentCreator; - public IndexingDocumentCreator(XmlDocumentCreator xmlDocumentCreator, - CsvDocumentCreator csvDocumentCreator, - JsonDocumentCreator jsonDocumentCreator) { + public IndexingDocumentCreator( + XmlDocumentCreator xmlDocumentCreator, + CsvDocumentCreator csvDocumentCreator, + JsonDocumentCreator jsonDocumentCreator) { this.xmlDocumentCreator = xmlDocumentCreator; this.csvDocumentCreator = csvDocumentCreator; this.jsonDocumentCreator = jsonDocumentCreator; @@ -66,35 +69,37 @@ public IndexingDocumentCreator(XmlDocumentCreator xmlDocumentCreator, /** * Creates a list of schema-less SolrInputDocument objects from a JSON string. * - *

This method delegates JSON processing to the JsonDocumentProcessor utility class.

+ *

This method delegates JSON processing to the JsonDocumentProcessor utility class. * * @param json JSON string containing document data (must be an array) * @return list of SolrInputDocument objects ready for indexing * @throws DocumentProcessingException if JSON parsing fails or the structure is invalid * @see JsonDocumentCreator */ - public List createSchemalessDocumentsFromJson(String json) throws DocumentProcessingException { + public List createSchemalessDocumentsFromJson(String json) + throws DocumentProcessingException { return jsonDocumentCreator.create(json); } /** * Creates a list of schema-less SolrInputDocument objects from a CSV string. * - *

This method delegates CSV processing to the CsvDocumentProcessor utility class.

+ *

This method delegates CSV processing to the CsvDocumentProcessor utility class. * * @param csv CSV string containing document data (first row must be headers) * @return list of SolrInputDocument objects ready for indexing * @throws DocumentProcessingException if CSV parsing fails or the structure is invalid * @see CsvDocumentCreator */ - public List createSchemalessDocumentsFromCsv(String csv) throws DocumentProcessingException { + public List createSchemalessDocumentsFromCsv(String csv) + throws DocumentProcessingException { return csvDocumentCreator.create(csv); } /** * Creates a list of schema-less SolrInputDocument objects from an XML string. * - *

This method delegates XML processing to the XmlDocumentProcessor utility class.

+ *

This method delegates XML processing to the XmlDocumentProcessor utility class. * * @param xml XML string containing document data * @return list of SolrInputDocument objects ready for indexing @@ -111,10 +116,14 @@ public List createSchemalessDocumentsFromXml(String xml) byte[] xmlBytes = xml.getBytes(StandardCharsets.UTF_8); if (xmlBytes.length > MAX_XML_SIZE_BYTES) { - throw new IllegalArgumentException("XML document too large: " + xmlBytes.length + " bytes (max: " + MAX_XML_SIZE_BYTES + ")"); + throw new IllegalArgumentException( + "XML document too large: " + + xmlBytes.length + + " bytes (max: " + + MAX_XML_SIZE_BYTES + + ")"); } return xmlDocumentCreator.create(xml); } - -} \ No newline at end of file +} 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 0862c4e..266ebd7 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 @@ -18,21 +18,20 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.solr.common.SolrInputDocument; -import org.springframework.stereotype.Component; - import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; +import org.apache.solr.common.SolrInputDocument; +import org.springframework.stereotype.Component; /** * Utility class for processing JSON documents and converting them to SolrInputDocument objects. * - *

This class handles the conversion of JSON documents into Solr-compatible format - * using a schema-less approach where Solr automatically detects field types.

+ *

This class handles the conversion of JSON documents into Solr-compatible format using a + * schema-less approach where Solr automatically detects field types. */ @Component public class JsonDocumentCreator implements SolrDocumentCreator { @@ -42,32 +41,37 @@ public class JsonDocumentCreator implements SolrDocumentCreator { /** * Creates a list of schema-less SolrInputDocument objects from a JSON string. * - *

This method implements a flexible document conversion strategy that allows Solr - * to automatically detect field types without requiring predefined schema configuration. - * It processes complex JSON structures by flattening nested objects and handling arrays - * appropriately for Solr's multi-valued field support.

+ *

This method implements a flexible document conversion strategy that allows Solr to + * automatically detect field types without requiring predefined schema configuration. It + * processes complex JSON structures by flattening nested objects and handling arrays + * appropriately for Solr's multi-valued field support. + * + *

Schema-less Benefits: * - *

Schema-less Benefits:

*
    - *
  • Flexibility: No need to predefine field types in schema
  • - *
  • Rapid Prototyping: Quick iteration on document structures
  • - *
  • Type Detection: Solr automatically infers optimal field types
  • - *
  • Dynamic Fields: Support for varying document structures
  • + *
  • Flexibility: No need to predefine field types in schema + *
  • Rapid Prototyping: Quick iteration on document structures + *
  • Type Detection: Solr automatically infers optimal field types + *
  • Dynamic Fields: Support for varying document structures *
* - *

JSON Processing Rules:

+ *

JSON Processing Rules: + * *

    - *
  • Nested Objects: Flattened using underscore notation (e.g., "user.name" → "user_name")
  • - *
  • Arrays: Non-object arrays converted to multi-valued fields
  • - *
  • Null Values: Ignored and not indexed
  • - *
  • Object Arrays: Skipped to avoid complex nested structures
  • + *
  • Nested Objects: Flattened using underscore notation (e.g., "user.name" + * → "user_name") + *
  • Arrays: Non-object arrays converted to multi-valued fields + *
  • Null Values: Ignored and not indexed + *
  • Object Arrays: Skipped to avoid complex nested structures *
* - *

Field Name Sanitization:

- *

Field names are automatically sanitized to ensure Solr compatibility by removing - * special characters and converting to lowercase with underscore separators.

+ *

Field Name Sanitization: + * + *

Field names are automatically sanitized to ensure Solr compatibility by removing special + * characters and converting to lowercase with underscore separators. + * + *

Example Transformations: * - *

Example Transformations:

*
{@code
      * Input:  {"user":{"name":"John","age":30},"tags":["tech","java"]}
      * Output: {user_name:"John", user_age:30, tags:["tech","java"]}
@@ -75,14 +79,16 @@ public class JsonDocumentCreator implements SolrDocumentCreator {
      *
      * @param json JSON string containing document data (must be an array)
      * @return list of SolrInputDocument objects ready for indexing
-     * @throws DocumentProcessingException if JSON parsing fails, input validation fails, or the structure is invalid
+     * @throws DocumentProcessingException if JSON parsing fails, input validation fails, or the
+     *     structure is invalid
      * @see SolrInputDocument
      * @see #addAllFieldsFlat(SolrInputDocument, JsonNode, String)
      * @see FieldNameSanitizer#sanitizeFieldName(String)
      */
     public List create(String json) throws DocumentProcessingException {
         if (json.getBytes(StandardCharsets.UTF_8).length > MAX_INPUT_SIZE_BYTES) {
-            throw new DocumentProcessingException("Input too large: exceeds maximum size of " + MAX_INPUT_SIZE_BYTES + " bytes");
+            throw new DocumentProcessingException(
+                    "Input too large: exceeds maximum size of " + MAX_INPUT_SIZE_BYTES + " bytes");
         }
 
         List documents = new ArrayList<>();
@@ -110,36 +116,41 @@ public List create(String json) throws DocumentProcessingExce
     /**
      * Recursively flattens JSON nodes and adds them as fields to a SolrInputDocument.
      *
-     * 

This method implements the core logic for converting nested JSON structures - * into flat field names that Solr can efficiently index and search. It handles - * various JSON node types appropriately while maintaining data integrity.

+ *

This method implements the core logic for converting nested JSON structures into flat + * field names that Solr can efficiently index and search. It handles various JSON node types + * appropriately while maintaining data integrity. + * + *

Processing Logic: * - *

Processing Logic:

*
    - *
  • Null Values: Skipped to avoid indexing empty fields
  • - *
  • Arrays: Non-object items converted to multi-valued fields
  • - *
  • Objects: Recursively flattened with prefix concatenation
  • - *
  • Primitives: Directly added with appropriate type conversion
  • + *
  • Null Values: Skipped to avoid indexing empty fields + *
  • Arrays: Non-object items converted to multi-valued fields + *
  • Objects: Recursively flattened with prefix concatenation + *
  • Primitives: Directly added with appropriate type conversion *
* - * @param doc the SolrInputDocument to add fields to - * @param node the JSON node to process + * @param doc the SolrInputDocument to add fields to + * @param node the JSON node to process * @param prefix current field name prefix for nested object flattening * @see #convertJsonValue(JsonNode) * @see FieldNameSanitizer#sanitizeFieldName(String) */ private void addAllFieldsFlat(SolrInputDocument doc, JsonNode node, String prefix) { Set> fields = node.properties(); - fields.forEach(field -> processFieldValue(doc, field.getValue(), - FieldNameSanitizer.sanitizeFieldName(prefix + field.getKey()))); + fields.forEach( + field -> + processFieldValue( + doc, + field.getValue(), + FieldNameSanitizer.sanitizeFieldName(prefix + field.getKey()))); } /** - * Processes the provided field value and adds it to the given SolrInputDocument. - * Handles cases where the field value is an array, object, or a simple value. + * Processes the provided field value and adds it to the given SolrInputDocument. Handles cases + * where the field value is an array, object, or a simple value. * - * @param doc the SolrInputDocument to which the field value will be added - * @param value the JsonNode representing the field value to be processed + * @param doc the SolrInputDocument to which the field value will be added + * @param value the JsonNode representing the field value to be processed * @param fieldName the name of the field to be added to the SolrInputDocument */ private void processFieldValue(SolrInputDocument doc, JsonNode value, String fieldName) { @@ -157,12 +168,13 @@ private void processFieldValue(SolrInputDocument doc, JsonNode value, String fie } /** - * Processes a JSON array field and adds its non-object elements to the specified field - * in the given SolrInputDocument. + * Processes a JSON array field and adds its non-object elements to the specified field in the + * given SolrInputDocument. * - * @param doc the SolrInputDocument to which the processed field will be added + * @param doc the SolrInputDocument to which the processed field will be added * @param arrayValue the JSON array node to process - * @param fieldName the name of the field in the SolrInputDocument to which the array values will be added + * @param fieldName the name of the field in the SolrInputDocument to which the array values + * will be added */ private void processArrayField(SolrInputDocument doc, JsonNode arrayValue, String fieldName) { List values = new ArrayList<>(); @@ -179,17 +191,18 @@ private void processArrayField(SolrInputDocument doc, JsonNode arrayValue, Strin /** * Converts a JsonNode value to the appropriate Java object type for Solr indexing. * - *

This method provides type-aware conversion of JSON values to their corresponding - * Java types, ensuring that Solr receives properly typed data for optimal field - * type detection and indexing performance.

+ *

This method provides type-aware conversion of JSON values to their corresponding Java + * types, ensuring that Solr receives properly typed data for optimal field type detection and + * indexing performance. + * + *

Supported Type Conversions: * - *

Supported Type Conversions:

*
    - *
  • Boolean: JSON boolean → Java Boolean
  • - *
  • Integer: JSON number (int range) → Java Integer
  • - *
  • Long: JSON number (long range) → Java Long
  • - *
  • Double: JSON number (decimal) → Java Double
  • - *
  • String: All other values → Java String
  • + *
  • Boolean: JSON boolean → Java Boolean + *
  • Integer: JSON number (int range) → Java Integer + *
  • Long: JSON number (long range) → Java Long + *
  • Double: JSON number (decimal) → Java Double + *
  • String: All other values → Java String *
* * @param value the JsonNode value to convert @@ -203,5 +216,4 @@ private Object convertJsonValue(JsonNode value) { if (value.isInt()) return value.asInt(); return value.asText(); } - -} \ No newline at end of file +} diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/SolrDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/SolrDocumentCreator.java index cd5072c..35ef4e5 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/SolrDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/SolrDocumentCreator.java @@ -16,34 +16,38 @@ */ package org.apache.solr.mcp.server.indexing.documentcreator; -import org.apache.solr.common.SolrInputDocument; - import java.util.List; +import org.apache.solr.common.SolrInputDocument; /** * Interface defining the contract for creating SolrInputDocument objects from various data formats. * - *

This interface provides a unified abstraction for converting different document formats - * (JSON, CSV, XML, etc.) into Solr-compatible SolrInputDocument objects. Implementations - * handle format-specific parsing and field sanitization to ensure proper Solr indexing.

+ *

This interface provides a unified abstraction for converting different document formats (JSON, + * CSV, XML, etc.) into Solr-compatible SolrInputDocument objects. Implementations handle + * format-specific parsing and field sanitization to ensure proper Solr indexing. + * + *

Design Principles: * - *

Design Principles:

*
    - *
  • Format Agnostic: Common interface for all document types
  • - *
  • Schema-less Processing: Supports dynamic field creation without predefined schema
  • - *
  • Error Handling: Consistent exception handling across implementations
  • - *
  • Field Sanitization: Automatic cleanup of field names for Solr compatibility
  • + *
  • Format Agnostic: Common interface for all document types + *
  • Schema-less Processing: Supports dynamic field creation without predefined + * schema + *
  • Error Handling: Consistent exception handling across implementations + *
  • Field Sanitization: Automatic cleanup of field names for Solr + * compatibility *
* - *

Implementation Guidelines:

+ *

Implementation Guidelines: + * *

    - *
  • Handle null or empty input gracefully
  • - *
  • Sanitize field names using {@link FieldNameSanitizer}
  • - *
  • Preserve original data types where possible
  • - *
  • Throw {@link DocumentProcessingException} for processing errors
  • + *
  • Handle null or empty input gracefully + *
  • Sanitize field names using {@link FieldNameSanitizer} + *
  • Preserve original data types where possible + *
  • Throw {@link DocumentProcessingException} for processing errors *
* - *

Usage Example:

+ *

Usage Example: + * *

{@code
  * SolrDocumentCreator creator = new JsonDocumentCreator();
  * String jsonData = "[{\"title\":\"Document 1\",\"content\":\"Content here\"}]";
@@ -61,32 +65,36 @@ public interface SolrDocumentCreator {
     /**
      * Creates a list of SolrInputDocument objects from the provided content string.
      *
-     * 

This method parses the input content according to the specific format handled by - * the implementing class (JSON, CSV, XML, etc.) and converts it into a list of - * SolrInputDocument objects ready for indexing.

+ *

This method parses the input content according to the specific format handled by the + * implementing class (JSON, CSV, XML, etc.) and converts it into a list of SolrInputDocument + * objects ready for indexing. + * + *

Processing Behavior: * - *

Processing Behavior:

*
    - *
  • Field Sanitization: All field names are sanitized for Solr compatibility
  • - *
  • Type Preservation: Original data types are maintained where possible
  • - *
  • Multiple Documents: Single content string may produce multiple documents
  • - *
  • Error Handling: Invalid content results in DocumentProcessingException
  • + *
  • Field Sanitization: All field names are sanitized for Solr + * compatibility + *
  • Type Preservation: Original data types are maintained where possible + *
  • Multiple Documents: Single content string may produce multiple + * documents + *
  • Error Handling: Invalid content results in DocumentProcessingException *
* - *

Input Validation:

+ *

Input Validation: + * *

    - *
  • Null input should be handled gracefully (implementation-dependent)
  • - *
  • Empty input should return empty list
  • - *
  • Malformed content should throw DocumentProcessingException
  • + *
  • Null input should be handled gracefully (implementation-dependent) + *
  • Empty input should return empty list + *
  • Malformed content should throw DocumentProcessingException *
* * @param content the content string to be parsed and converted to SolrInputDocument objects. - * The format depends on the implementing class (JSON array, CSV data, XML, etc.) - * @return a list of SolrInputDocument objects created from the parsed content. - * Returns empty list if content is empty or contains no valid documents + * The format depends on the implementing class (JSON array, CSV data, XML, etc.) + * @return a list of SolrInputDocument objects created from the parsed content. Returns empty + * list if content is empty or contains no valid documents * @throws DocumentProcessingException if the content cannot be parsed or converted due to - * format errors, invalid structure, or processing failures - * @throws IllegalArgumentException if content is null (implementation-dependent) + * format errors, invalid structure, or processing failures + * @throws IllegalArgumentException if content is null (implementation-dependent) */ List create(String content) throws DocumentProcessingException; } diff --git a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/XmlDocumentCreator.java b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/XmlDocumentCreator.java index 6c20b2a..827d574 100644 --- a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/XmlDocumentCreator.java +++ b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/XmlDocumentCreator.java @@ -16,18 +16,6 @@ */ package org.apache.solr.mcp.server.indexing.documentcreator; -import org.apache.solr.common.SolrInputDocument; -import org.springframework.stereotype.Component; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -35,12 +23,23 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.solr.common.SolrInputDocument; +import org.springframework.stereotype.Component; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; /** * Utility class for processing XML documents and converting them to SolrInputDocument objects. * - *

This class handles the conversion of XML documents into Solr-compatible format - * using a schema-less approach where Solr automatically detects field types.

+ *

This class handles the conversion of XML documents into Solr-compatible format using a + * schema-less approach where Solr automatically detects field types. */ @Component public class XmlDocumentCreator implements SolrDocumentCreator { @@ -48,17 +47,18 @@ public class XmlDocumentCreator implements SolrDocumentCreator { /** * Creates a list of SolrInputDocument objects from XML content. * - *

This method parses the XML and creates documents based on the structure: - * - If the XML has multiple child elements with the same tag name (indicating repeated structures), - * each child element becomes a separate document - * - Otherwise, the entire XML structure is treated as a single document

+ *

This method parses the XML and creates documents based on the structure: - If the XML has + * multiple child elements with the same tag name (indicating repeated structures), each child + * element becomes a separate document - Otherwise, the entire XML structure is treated as a + * single document * - *

This approach is flexible and doesn't rely on hardcoded element names, - * allowing it to work with any XML structure.

+ *

This approach is flexible and doesn't rely on hardcoded element names, allowing it to work + * with any XML structure. * * @param xml the XML content to process * @return list of SolrInputDocument objects ready for indexing - * @throws DocumentProcessingException if XML parsing fails, parser configuration fails, or structural errors occur + * @throws DocumentProcessingException if XML parsing fails, parser configuration fails, or + * structural errors occur */ public List create(String xml) throws DocumentProcessingException { try { @@ -67,26 +67,26 @@ public List create(String xml) throws DocumentProcessingExcep } catch (ParserConfigurationException e) { throw new DocumentProcessingException("Failed to configure XML parser", e); } catch (SAXException e) { - throw new DocumentProcessingException("Failed to parse XML document: structural error", e); + throw new DocumentProcessingException( + "Failed to parse XML document: structural error", e); } catch (IOException e) { throw new DocumentProcessingException("Failed to read XML document", e); } } - /** - * Parses XML string into a DOM Element. - */ - private Element parseXmlDocument(String xml) throws ParserConfigurationException, SAXException, IOException { + /** Parses XML string into a DOM Element. */ + private Element parseXmlDocument(String xml) + throws ParserConfigurationException, SAXException, IOException { DocumentBuilderFactory factory = createSecureDocumentBuilderFactory(); DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); + Document doc = + builder.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); return doc.getDocumentElement(); } - /** - * Creates a secure DocumentBuilderFactory with XXE protection. - */ - private DocumentBuilderFactory createSecureDocumentBuilderFactory() throws ParserConfigurationException { + /** Creates a secure DocumentBuilderFactory with XXE protection. */ + private DocumentBuilderFactory createSecureDocumentBuilderFactory() + throws ParserConfigurationException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); @@ -97,12 +97,10 @@ private DocumentBuilderFactory createSecureDocumentBuilderFactory() throws Parse return factory; } - /** - * Processes the root element and determines document structure strategy. - */ + /** Processes the root element and determines document structure strategy. */ private List processRootElement(Element rootElement) { List childElements = extractChildElements(rootElement); - + if (shouldTreatChildrenAsDocuments(childElements)) { return createDocumentsFromChildren(childElements); } else { @@ -110,42 +108,36 @@ private List processRootElement(Element rootElement) { } } - /** - * Extracts child elements from the root element. - */ + /** Extracts child elements from the root element. */ private List extractChildElements(Element rootElement) { NodeList children = rootElement.getChildNodes(); List childElements = new ArrayList<>(); - + for (int i = 0; i < children.getLength(); i++) { if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { childElements.add((Element) children.item(i)); } } - + return childElements; } - /** - * Determines if child elements should be treated as separate documents. - */ + /** Determines if child elements should be treated as separate documents. */ private boolean shouldTreatChildrenAsDocuments(List childElements) { Map childElementCounts = new HashMap<>(); - + for (Element child : childElements) { String tagName = child.getTagName(); childElementCounts.put(tagName, childElementCounts.getOrDefault(tagName, 0) + 1); } - + return childElementCounts.values().stream().anyMatch(count -> count > 1); } - /** - * Creates documents from child elements (multiple documents strategy). - */ + /** Creates documents from child elements (multiple documents strategy). */ private List createDocumentsFromChildren(List childElements) { List documents = new ArrayList<>(); - + for (Element childElement : childElements) { SolrInputDocument solrDoc = new SolrInputDocument(); addXmlElementFields(solrDoc, childElement, ""); @@ -153,51 +145,51 @@ private List createDocumentsFromChildren(List childE documents.add(solrDoc); } } - + return documents; } - /** - * Creates a single document from the root element. - */ + /** Creates a single document from the root element. */ private List createSingleDocument(Element rootElement) { List documents = new ArrayList<>(); SolrInputDocument solrDoc = new SolrInputDocument(); addXmlElementFields(solrDoc, rootElement, ""); - + if (!solrDoc.isEmpty()) { documents.add(solrDoc); } - + return documents; } /** * Recursively processes XML elements and adds them as fields to a SolrInputDocument. * - *

This method implements the core logic for converting nested XML structures - * into flat field names that Solr can efficiently index and search. It handles - * both element content and attributes while maintaining data integrity.

+ *

This method implements the core logic for converting nested XML structures into flat field + * names that Solr can efficiently index and search. It handles both element content and + * attributes while maintaining data integrity. + * + *

Processing Logic: * - *

Processing Logic:

*
    - *
  • Attributes: Converted to fields with "_attr" suffix
  • - *
  • Text Content: Element text content indexed directly
  • - *
  • Child Elements: Recursively processed with prefix concatenation
  • - *
  • Empty Elements: Skipped to avoid indexing empty fields
  • - *
  • Repeated Elements: Combined into multi-valued fields
  • + *
  • Attributes: Converted to fields with "_attr" suffix + *
  • Text Content: Element text content indexed directly + *
  • Child Elements: Recursively processed with prefix concatenation + *
  • Empty Elements: Skipped to avoid indexing empty fields + *
  • Repeated Elements: Combined into multi-valued fields *
* - *

Field Naming Convention:

+ *

Field Naming Convention: + * *

    - *
  • Nested elements: parent_child (e.g., author_name)
  • - *
  • Attributes: elementname_attr (e.g., id_attr)
  • - *
  • All field names are sanitized for Solr compatibility
  • + *
  • Nested elements: parent_child (e.g., author_name) + *
  • Attributes: elementname_attr (e.g., id_attr) + *
  • All field names are sanitized for Solr compatibility *
* - * @param doc the SolrInputDocument to add fields to + * @param doc the SolrInputDocument to add fields to * @param element the XML element to process - * @param prefix current field name prefix for nested element flattening + * @param prefix current field name prefix for nested element flattening * @see FieldNameSanitizer#sanitizeFieldName(String) */ private void addXmlElementFields(SolrInputDocument doc, Element element, String prefix) { @@ -213,10 +205,9 @@ private void addXmlElementFields(SolrInputDocument doc, Element element, String processXmlChildElements(doc, children, currentPrefix); } - /** - * Processes XML element attributes and adds them as fields to the document. - */ - private void processXmlAttributes(SolrInputDocument doc, Element element, String prefix, String currentPrefix) { + /** Processes XML element attributes and adds them as fields to the document. */ + private void processXmlAttributes( + SolrInputDocument doc, Element element, String prefix, String currentPrefix) { if (!element.hasAttributes()) { return; } @@ -233,9 +224,7 @@ private void processXmlAttributes(SolrInputDocument doc, Element element, String } } - /** - * Checks if the node list contains any child elements. - */ + /** Checks if the node list contains any child elements. */ private boolean hasChildElements(NodeList children) { for (int i = 0; i < children.getLength(); i++) { if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { @@ -245,12 +234,14 @@ private boolean hasChildElements(NodeList children) { return false; } - /** - * Processes XML text content and adds it as a field to the document. - */ - private void processXmlTextContent(SolrInputDocument doc, String elementName, - String currentPrefix, String prefix, boolean hasChildElements, - NodeList children) { + /** Processes XML text content and adds it as a field to the document. */ + private void processXmlTextContent( + SolrInputDocument doc, + String elementName, + String currentPrefix, + String prefix, + boolean hasChildElements, + NodeList children) { String textContent = extractTextContent(children); if (!textContent.isEmpty()) { String fieldName = prefix.isEmpty() ? elementName : currentPrefix; @@ -258,9 +249,7 @@ private void processXmlTextContent(SolrInputDocument doc, String elementName, } } - /** - * Extracts text content from child nodes. - */ + /** Extracts text content from child nodes. */ private String extractTextContent(NodeList children) { StringBuilder textContent = new StringBuilder(); @@ -277,10 +266,9 @@ private String extractTextContent(NodeList children) { return textContent.toString().trim(); } - /** - * Recursively processes XML child elements. - */ - private void processXmlChildElements(SolrInputDocument doc, NodeList children, String currentPrefix) { + /** Recursively processes XML child elements. */ + private void processXmlChildElements( + SolrInputDocument doc, NodeList children, String currentPrefix) { for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { @@ -288,5 +276,4 @@ private void processXmlChildElements(SolrInputDocument doc, NodeList children, S } } } - -} \ No newline at end of file +} 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 3b02ae3..5f42008 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 @@ -16,6 +16,12 @@ */ package org.apache.solr.mcp.server.metadata; +import static org.apache.solr.mcp.server.metadata.CollectionUtils.*; + +import java.io.IOException; +import java.util.ArrayList; +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; @@ -34,64 +40,68 @@ import org.springaicommunity.mcp.annotation.McpToolParam; import org.springframework.stereotype.Service; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import static org.apache.solr.mcp.server.metadata.CollectionUtils.*; - /** - * Spring Service providing comprehensive Solr collection management and monitoring capabilities - * for Model Context Protocol (MCP) clients. - * - *

This service acts as the primary interface for collection-level operations in the Solr MCP Server, - * providing tools for collection discovery, metrics gathering, health monitoring, and performance analysis. - * It bridges the gap between MCP clients (like Claude Desktop) and Apache Solr through the SolrJ client library.

- * - *

Core Capabilities:

+ * Spring Service providing comprehensive Solr collection management and monitoring capabilities for + * Model Context Protocol (MCP) clients. + * + *

This service acts as the primary interface for collection-level operations in the Solr MCP + * Server, providing tools for collection discovery, metrics gathering, health monitoring, and + * performance analysis. It bridges the gap between MCP clients (like Claude Desktop) and Apache + * Solr through the SolrJ client library. + * + *

Core Capabilities: + * *

    - *
  • Collection Discovery: Lists available collections/cores with automatic SolrCloud vs standalone detection
  • - *
  • Performance Monitoring: Comprehensive metrics collection including index, query, cache, and handler statistics
  • - *
  • Health Monitoring: Real-time health checks with availability and performance indicators
  • - *
  • Shard-Aware Operations: Intelligent handling of SolrCloud shard names and collection name extraction
  • + *
  • Collection Discovery: Lists available collections/cores with automatic + * SolrCloud vs standalone detection + *
  • Performance Monitoring: Comprehensive metrics collection including index, + * query, cache, and handler statistics + *
  • Health Monitoring: Real-time health checks with availability and + * performance indicators + *
  • Shard-Aware Operations: Intelligent handling of SolrCloud shard names and + * collection name extraction *
* - *

Implementation Details:

- *

This class uses extensively documented constants for all API parameters, field names, and paths to ensure - * maintainability and reduce the risk of typos. All string literals have been replaced with well-named constants - * that are organized by category (API parameters, response parsing keys, handler paths, statistics fields, etc.).

- * - *

MCP Tool Integration:

- *

Methods annotated with {@code @McpTool} are automatically exposed as MCP tools that can be invoked - * by AI clients. These tools provide natural language interfaces to Solr operations.

- * - *

Supported Solr Deployments:

+ *

Implementation Details: + * + *

This class uses extensively documented constants for all API parameters, field names, and + * paths to ensure maintainability and reduce the risk of typos. All string literals have been + * replaced with well-named constants that are organized by category (API parameters, response + * parsing keys, handler paths, statistics fields, etc.). + * + *

MCP Tool Integration: + * + *

Methods annotated with {@code @McpTool} are automatically exposed as MCP tools that can be + * invoked by AI clients. These tools provide natural language interfaces to Solr operations. + * + *

Supported Solr Deployments: + * *

    - *
  • SolrCloud: Distributed mode using Collections API
  • - *
  • Standalone: Single-node mode using Core Admin API
  • + *
  • SolrCloud: Distributed mode using Collections API + *
  • Standalone: Single-node mode using Core Admin API *
- * - *

Error Handling:

+ * + *

Error Handling: + * *

The service implements robust error handling with graceful degradation. Failed operations * return null values rather than throwing exceptions (except where validation requires it), - * allowing partial metrics collection when some endpoints are unavailable.

- * - *

Example Usage:

+ * allowing partial metrics collection when some endpoints are unavailable. + * + *

Example Usage: + * *

{@code
  * // List all available collections
  * List collections = collectionService.listCollections();
- * 
+ *
  * // Get comprehensive metrics for a collection
  * SolrMetrics metrics = collectionService.getCollectionStats("my_collection");
- * 
+ *
  * // Check collection health
  * SolrHealthStatus health = collectionService.checkHealth("my_collection");
  * }
* * @version 0.0.1 * @since 0.0.1 - * * @see SolrMetrics * @see SolrHealthStatus * @see org.apache.solr.client.solrj.SolrClient @@ -103,14 +113,10 @@ public class CollectionService { // Constants for API Parameters and Paths // ======================================== - /** - * Category parameter value for cache-related MBeans requests - */ + /** Category parameter value for cache-related MBeans requests */ private static final String CACHE_CATEGORY = "CACHE"; - /** - * Category parameter value for query handler MBeans requests - */ + /** Category parameter value for query handler MBeans requests */ private static final String QUERY_HANDLER_CATEGORY = "QUERYHANDLER"; /** Combined category parameter value for both query and update handler MBeans requests */ @@ -221,12 +227,11 @@ public class CollectionService { /** * Constructs a new CollectionService with the required dependencies. - * - *

This constructor is automatically called by Spring's dependency injection - * framework during application startup.

- * - * @param solrClient the SolrJ client instance for communicating with Solr * + *

This constructor is automatically called by Spring's dependency injection framework during + * application startup. + * + * @param solrClient the SolrJ client instance for communicating with Solr * @see SolrClient * @see SolrConfigurationProperties */ @@ -236,28 +241,30 @@ public CollectionService(SolrClient solrClient) { /** * Lists all available Solr collections or cores in the cluster. - * - *

This method automatically detects the Solr deployment type and uses the appropriate API:

+ * + *

This method automatically detects the Solr deployment type and uses the appropriate API: + * *

    - *
  • SolrCloud: Uses Collections API to list distributed collections
  • - *
  • Standalone: Uses Core Admin API to list individual cores
  • + *
  • SolrCloud: Uses Collections API to list distributed collections + *
  • Standalone: Uses Core Admin API to list individual cores *
- * - *

In SolrCloud environments, the returned names may include shard identifiers - * (e.g., "films_shard1_replica_n1"). Use {@link #extractCollectionName(String)} - * to get the base collection name if needed.

- * - *

Error Handling:

- *

If the operation fails due to connectivity issues or API errors, an empty list - * is returned rather than throwing an exception, allowing the application to continue - * functioning with degraded capabilities.

- * - *

MCP Tool Usage:

- *

This method is exposed as an MCP tool and can be invoked by AI clients with - * natural language requests like "list all collections" or "show me available databases".

- * + * + *

In SolrCloud environments, the returned names may include shard identifiers (e.g., + * "films_shard1_replica_n1"). Use {@link #extractCollectionName(String)} to get the base + * collection name if needed. + * + *

Error Handling: + * + *

If the operation fails due to connectivity issues or API errors, an empty list is returned + * rather than throwing an exception, allowing the application to continue functioning with + * degraded capabilities. + * + *

MCP Tool Usage: + * + *

This method is exposed as an MCP tool and can be invoked by AI clients with natural + * language requests like "list all collections" or "show me available databases". + * * @return a list of collection/core names, or an empty list if unable to retrieve them - * * @see CollectionAdminRequest.List * @see CoreAdminRequest */ @@ -270,7 +277,8 @@ public List listCollections() { CollectionAdminResponse response = request.process(solrClient); @SuppressWarnings("unchecked") - List collections = (List) response.getResponse().get(COLLECTIONS_KEY); + List collections = + (List) response.getResponse().get(COLLECTIONS_KEY); return collections != null ? collections : new ArrayList<>(); } else { // For standalone Solr - use Core Admin API @@ -292,46 +300,53 @@ public List listCollections() { /** * Retrieves comprehensive performance metrics and statistics for a specified Solr collection. - * + * *

This method aggregates metrics from multiple Solr endpoints to provide a complete - * performance profile including index health, query performance, cache utilization, - * and request handler statistics.

- * - *

Collected Metrics:

+ * performance profile including index health, query performance, cache utilization, and request + * handler statistics. + * + *

Collected Metrics: + * *

    - *
  • Index Statistics: Document counts, segment information (via Luke handler)
  • - *
  • Query Performance: Response times, result counts, relevance scores
  • - *
  • Cache Utilization: Hit ratios, eviction rates for all cache types
  • - *
  • Handler Performance: Request volumes, error rates, throughput metrics
  • + *
  • Index Statistics: Document counts, segment information (via Luke + * handler) + *
  • Query Performance: Response times, result counts, relevance scores + *
  • Cache Utilization: Hit ratios, eviction rates for all cache types + *
  • Handler Performance: Request volumes, error rates, throughput metrics *
- * - *

Collection Name Handling:

+ * + *

Collection Name Handling: + * *

Supports both collection names and shard names. If a shard name like - * "films_shard1_replica_n1" is provided, it will be automatically converted - * to the base collection name "films" for API calls.

- * - *

Validation:

- *

The method validates that the specified collection exists before attempting - * to collect metrics. If the collection is not found, an {@code IllegalArgumentException} - * is thrown with a descriptive error message.

- * - *

MCP Tool Usage:

+ * "films_shard1_replica_n1" is provided, it will be automatically converted to the base + * collection name "films" for API calls. + * + *

Validation: + * + *

The method validates that the specified collection exists before attempting to collect + * metrics. If the collection is not found, an {@code IllegalArgumentException} is thrown with a + * descriptive error message. + * + *

MCP Tool Usage: + * *

Exposed as an MCP tool for natural language queries like "get metrics for my_collection" - * or "show me performance stats for the search index".

- * - * @param collection the name of the collection to analyze (supports both collection and shard names) - * @return comprehensive metrics object containing all collected statistics + * or "show me performance stats for the search index". * + * @param collection the name of the collection to analyze (supports both collection and shard + * names) + * @return comprehensive metrics object containing all collected statistics * @throws IllegalArgumentException if the specified collection does not exist * @throws SolrServerException if there are errors communicating with Solr * @throws IOException if there are I/O errors during communication - * * @see SolrMetrics * @see LukeRequest * @see #extractCollectionName(String) */ @McpTool(description = "Get stats/metrics on a Solr collection") - public SolrMetrics getCollectionStats(@McpToolParam(description = "Solr collection to get stats/metrics for") String collection) throws SolrServerException, IOException { + public SolrMetrics getCollectionStats( + @McpToolParam(description = "Solr collection to get stats/metrics for") + String collection) + throws SolrServerException, IOException { // Extract actual collection name from shard name if needed String actualCollection = extractCollectionName(collection); @@ -346,38 +361,38 @@ public SolrMetrics getCollectionStats(@McpToolParam(description = "Solr collecti LukeResponse lukeResponse = lukeRequest.process(solrClient, actualCollection); // Query performance metrics - QueryResponse statsResponse = solrClient.query(actualCollection, - new SolrQuery(ALL_DOCUMENTS_QUERY).setRows(0)); + QueryResponse statsResponse = + solrClient.query(actualCollection, new SolrQuery(ALL_DOCUMENTS_QUERY).setRows(0)); return new SolrMetrics( buildIndexStats(lukeResponse), buildQueryStats(statsResponse), getCacheMetrics(actualCollection), getHandlerMetrics(actualCollection), - new Date() - ); + new Date()); } /** * Builds an IndexStats object from a Solr Luke response containing index metadata. - * - *

The Luke handler provides low-level Lucene index information including document - * counts, segment details, and field statistics. This method extracts the essential - * index health metrics for monitoring and analysis.

- * - *

Extracted Metrics:

+ * + *

The Luke handler provides low-level Lucene index information including document counts, + * segment details, and field statistics. This method extracts the essential index health + * metrics for monitoring and analysis. + * + *

Extracted Metrics: + * *

    - *
  • numDocs: Total number of documents excluding deleted ones
  • - *
  • segmentCount: Number of Lucene segments (performance indicator)
  • + *
  • numDocs: Total number of documents excluding deleted ones + *
  • segmentCount: Number of Lucene segments (performance indicator) *
- * - *

Performance Implications:

- *

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.

- * + * + *

Performance Implications: + * + *

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 lukeResponse the Luke response containing raw index information * @return IndexStats object with extracted and formatted metrics - * * @see IndexStats * @see LukeResponse */ @@ -387,34 +402,32 @@ public IndexStats buildIndexStats(LukeResponse lukeResponse) { // Extract index information using helper methods Integer segmentCount = getInteger(indexInfo, SEGMENT_COUNT_KEY); - return new IndexStats( - lukeResponse.getNumDocs(), - segmentCount - ); + return new IndexStats(lukeResponse.getNumDocs(), segmentCount); } /** * Builds a QueryStats object from a Solr query response containing performance metrics. - * - *

Extracts key performance indicators from a query execution including timing, - * result characteristics, and relevance scoring information. These metrics help - * identify query performance patterns and optimization opportunities.

- * - *

Extracted Metrics:

+ * + *

Extracts key performance indicators from a query execution including timing, result + * characteristics, and relevance scoring information. These metrics help identify query + * performance patterns and optimization opportunities. + * + *

Extracted Metrics: + * *

    - *
  • queryTime: Execution time in milliseconds
  • - *
  • totalResults: Total matching documents found
  • - *
  • start: Pagination offset (0-based)
  • - *
  • maxScore: Highest relevance score in results
  • + *
  • queryTime: Execution time in milliseconds + *
  • totalResults: Total matching documents found + *
  • start: Pagination offset (0-based) + *
  • maxScore: Highest relevance score in results *
- * - *

Performance Analysis:

- *

Query time metrics help identify slow queries that may need optimization, - * while result counts and scores provide insight into search effectiveness.

- * + * + *

Performance Analysis: + * + *

Query time metrics help identify slow queries that may need optimization, while result + * counts and scores provide insight into search effectiveness. + * * @param response the query response containing performance and result metadata * @return QueryStats object with extracted performance metrics - * * @see QueryStats * @see QueryResponse */ @@ -424,39 +437,40 @@ public QueryStats buildQueryStats(QueryResponse response) { response.getQTime(), response.getResults().getNumFound(), response.getResults().getStart(), - response.getResults().getMaxScore() - ); + response.getResults().getMaxScore()); } /** * Retrieves cache performance metrics for all cache types in a Solr collection. - * - *

Collects detailed cache utilization statistics from Solr's MBeans endpoint, - * providing insights into cache effectiveness and memory usage patterns. Cache - * performance directly impacts query response times and system efficiency.

- * - *

Monitored Cache Types:

+ * + *

Collects detailed cache utilization statistics from Solr's MBeans endpoint, providing + * insights into cache effectiveness and memory usage patterns. Cache performance directly + * impacts query response times and system efficiency. + * + *

Monitored Cache Types: + * *

    - *
  • Query Result Cache: Caches complete query results for identical searches
  • - *
  • Document Cache: Caches retrieved document field data
  • - *
  • Filter Cache: Caches filter query results for faceting and filtering
  • + *
  • Query Result Cache: Caches complete query results for identical + * searches + *
  • Document Cache: Caches retrieved document field data + *
  • Filter Cache: Caches filter query results for faceting and filtering *
- * - *

Key Performance Indicators:

+ * + *

Key Performance Indicators: + * *

    - *
  • Hit Ratio: Cache effectiveness (higher is better)
  • - *
  • Evictions: Memory pressure indicator
  • - *
  • Size: Current cache utilization
  • + *
  • Hit Ratio: Cache effectiveness (higher is better) + *
  • Evictions: Memory pressure indicator + *
  • Size: Current cache utilization *
- * - *

Error Handling:

- *

Returns {@code null} if cache statistics cannot be retrieved or if all - * cache types are empty/unavailable. This allows graceful degradation when - * cache monitoring is not available.

- * + * + *

Error Handling: + * + *

Returns {@code null} if cache statistics cannot be retrieved or if all cache types are + * empty/unavailable. This allows graceful degradation when cache monitoring is not available. + * * @param collection the collection name to retrieve cache metrics for * @return CacheStats object with all cache performance metrics, or null if unavailable - * * @see CacheStats * @see CacheInfo * @see #extractCacheStats(NamedList) @@ -480,11 +494,8 @@ public CacheStats getCacheMetrics(String collection) { String path = "/" + actualCollection + ADMIN_MBEANS_PATH; - GenericSolrRequest request = new GenericSolrRequest( - SolrRequest.METHOD.GET, - path, - params - ); + GenericSolrRequest request = + new GenericSolrRequest(SolrRequest.METHOD.GET, path, params); NamedList response = solrClient.request(request); CacheStats stats = extractCacheStats(response); @@ -502,45 +513,45 @@ public CacheStats getCacheMetrics(String collection) { /** * Checks if cache statistics are empty or contain no meaningful data. - * - *

Used to determine whether cache metrics are worth returning to clients. - * Empty cache stats typically indicate that caches are not configured or - * not yet populated with data.

- * + * + *

Used to determine whether cache metrics are worth returning to clients. Empty cache stats + * typically indicate that caches are not configured or not yet populated with data. + * * @param stats the cache statistics to evaluate * @return true if the stats are null or all cache types are null */ private boolean isCacheStatsEmpty(CacheStats stats) { - return stats == null || - (stats.queryResultCache() == null && - stats.documentCache() == null && - stats.filterCache() == null); + return stats == null + || (stats.queryResultCache() == null + && stats.documentCache() == null + && stats.filterCache() == null); } /** * Extracts cache performance statistics from Solr MBeans response data. - * - *

Parses the raw MBeans response to extract structured cache performance - * metrics for all available cache types. Each cache type provides detailed - * statistics including hit ratios, eviction rates, and current utilization.

- * - *

Parsed Cache Types:

+ * + *

Parses the raw MBeans response to extract structured cache performance metrics for all + * available cache types. Each cache type provides detailed statistics including hit ratios, + * eviction rates, and current utilization. + * + *

Parsed Cache Types: + * *

    - *
  • queryResultCache - Complete query result caching
  • - *
  • documentCache - Retrieved document data caching
  • - *
  • filterCache - Filter query result caching
  • + *
  • queryResultCache - Complete query result caching + *
  • documentCache - Retrieved document data caching + *
  • filterCache - Filter query result caching *
- * - *

For each cache type, the following metrics are extracted:

+ * + *

For each cache type, the following metrics are extracted: + * *

    - *
  • lookups, hits, hitratio - Performance effectiveness
  • - *
  • inserts, evictions - Memory management patterns
  • - *
  • size - Current utilization
  • + *
  • lookups, hits, hitratio - Performance effectiveness + *
  • inserts, evictions - Memory management patterns + *
  • size - Current utilization *
- * + * * @param mbeans the raw MBeans response from Solr admin endpoint * @return CacheStats object containing parsed metrics for all cache types - * * @see CacheStats * @see CacheInfo */ @@ -555,18 +566,19 @@ private CacheStats extractCacheStats(NamedList mbeans) { if (caches != null) { // Query result cache @SuppressWarnings("unchecked") - NamedList queryResultCache = (NamedList) caches.get(QUERY_RESULT_CACHE_KEY); + NamedList queryResultCache = + (NamedList) caches.get(QUERY_RESULT_CACHE_KEY); if (queryResultCache != null) { @SuppressWarnings("unchecked") NamedList stats = (NamedList) queryResultCache.get(STATS_KEY); - queryResultCacheInfo = new CacheInfo( - getLong(stats, LOOKUPS_FIELD), - getLong(stats, HITS_FIELD), - getFloat(stats, HITRATIO_FIELD), - getLong(stats, INSERTS_FIELD), - getLong(stats, EVICTIONS_FIELD), - getLong(stats, SIZE_FIELD) - ); + queryResultCacheInfo = + new CacheInfo( + getLong(stats, LOOKUPS_FIELD), + getLong(stats, HITS_FIELD), + getFloat(stats, HITRATIO_FIELD), + getLong(stats, INSERTS_FIELD), + getLong(stats, EVICTIONS_FIELD), + getLong(stats, SIZE_FIELD)); } // Document cache @@ -575,14 +587,14 @@ private CacheStats extractCacheStats(NamedList mbeans) { if (documentCache != null) { @SuppressWarnings("unchecked") NamedList stats = (NamedList) documentCache.get(STATS_KEY); - documentCacheInfo = new CacheInfo( - getLong(stats, LOOKUPS_FIELD), - getLong(stats, HITS_FIELD), - getFloat(stats, HITRATIO_FIELD), - getLong(stats, INSERTS_FIELD), - getLong(stats, EVICTIONS_FIELD), - getLong(stats, SIZE_FIELD) - ); + documentCacheInfo = + new CacheInfo( + getLong(stats, LOOKUPS_FIELD), + getLong(stats, HITS_FIELD), + getFloat(stats, HITRATIO_FIELD), + getLong(stats, INSERTS_FIELD), + getLong(stats, EVICTIONS_FIELD), + getLong(stats, SIZE_FIELD)); } // Filter cache @@ -591,14 +603,14 @@ private CacheStats extractCacheStats(NamedList mbeans) { if (filterCache != null) { @SuppressWarnings("unchecked") NamedList stats = (NamedList) filterCache.get(STATS_KEY); - filterCacheInfo = new CacheInfo( - getLong(stats, LOOKUPS_FIELD), - getLong(stats, HITS_FIELD), - getFloat(stats, HITRATIO_FIELD), - getLong(stats, INSERTS_FIELD), - getLong(stats, EVICTIONS_FIELD), - getLong(stats, SIZE_FIELD) - ); + filterCacheInfo = + new CacheInfo( + getLong(stats, LOOKUPS_FIELD), + getLong(stats, HITS_FIELD), + getFloat(stats, HITRATIO_FIELD), + getLong(stats, INSERTS_FIELD), + getLong(stats, EVICTIONS_FIELD), + getLong(stats, SIZE_FIELD)); } } @@ -607,32 +619,36 @@ private CacheStats extractCacheStats(NamedList mbeans) { /** * Retrieves request handler performance metrics for core Solr operations. - * - *

Collects detailed performance statistics for the primary request handlers - * that process search and update operations. Handler metrics provide insights - * into system throughput, error rates, and response time characteristics.

- * - *

Monitored Handlers:

+ * + *

Collects detailed performance statistics for the primary request handlers that process + * search and update operations. Handler metrics provide insights into system throughput, error + * rates, and response time characteristics. + * + *

Monitored Handlers: + * *

    - *
  • Select Handler ({@value #SELECT_HANDLER_PATH}): Processes search and query requests
  • - *
  • Update Handler ({@value #UPDATE_HANDLER_PATH}): Processes document indexing operations
  • + *
  • Select Handler ({@value #SELECT_HANDLER_PATH}): Processes search and + * query requests + *
  • Update Handler ({@value #UPDATE_HANDLER_PATH}): Processes document + * indexing operations *
- * - *

Performance Metrics:

+ * + *

Performance Metrics: + * *

    - *
  • Request Volume: Total requests processed
  • - *
  • Error Rates: Failed request counts and timeouts
  • - *
  • Performance: Average response times and throughput
  • + *
  • Request Volume: Total requests processed + *
  • Error Rates: Failed request counts and timeouts + *
  • Performance: Average response times and throughput *
- * - *

Error Handling:

- *

Returns {@code null} if handler statistics cannot be retrieved or if - * no meaningful handler data is available. This allows graceful degradation - * when handler monitoring endpoints are not accessible.

- * + * + *

Error Handling: + * + *

Returns {@code null} if handler statistics cannot be retrieved or if no meaningful handler + * data is available. This allows graceful degradation when handler monitoring endpoints are not + * accessible. + * * @param collection the collection name to retrieve handler metrics for * @return HandlerStats object with performance metrics for all handlers, or null if unavailable - * * @see HandlerStats * @see HandlerInfo * @see #extractHandlerStats(NamedList) @@ -655,11 +671,8 @@ public HandlerStats getHandlerMetrics(String collection) { String path = "/" + actualCollection + ADMIN_MBEANS_PATH; - GenericSolrRequest request = new GenericSolrRequest( - SolrRequest.METHOD.GET, - path, - params - ); + GenericSolrRequest request = + new GenericSolrRequest(SolrRequest.METHOD.GET, path, params); NamedList response = solrClient.request(request); HandlerStats stats = extractHandlerStats(response); @@ -677,42 +690,42 @@ public HandlerStats getHandlerMetrics(String collection) { /** * Checks if handler statistics are empty or contain no meaningful data. - * - *

Used to determine whether handler metrics are worth returning to clients. - * Empty handler stats typically indicate that handlers haven't processed any - * requests yet or statistics collection is not enabled.

- * + * + *

Used to determine whether handler metrics are worth returning to clients. Empty handler + * stats typically indicate that handlers haven't processed any requests yet or statistics + * collection is not enabled. + * * @param stats the handler statistics to evaluate * @return true if the stats are null or all handler types are null */ private boolean isHandlerStatsEmpty(HandlerStats stats) { - return stats == null || - (stats.selectHandler() == null && stats.updateHandler() == null); + return stats == null || (stats.selectHandler() == null && stats.updateHandler() == null); } /** * Extracts request handler performance statistics from Solr MBeans response data. - * - *

Parses the raw MBeans response to extract structured handler performance - * metrics for query and update operations. Each handler provides detailed - * statistics about request processing including volume, errors, and timing.

- * - *

Parsed Handler Types:

+ * + *

Parses the raw MBeans response to extract structured handler performance metrics for query + * and update operations. Each handler provides detailed statistics about request processing + * including volume, errors, and timing. + * + *

Parsed Handler Types: + * *

    - *
  • /select - Search and query request handler
  • - *
  • /update - Document indexing request handler
  • + *
  • /select - Search and query request handler + *
  • /update - Document indexing request handler *
- * - *

For each handler type, the following metrics are extracted:

+ * + *

For each handler type, the following metrics are extracted: + * *

    - *
  • requests, errors, timeouts - Volume and reliability
  • - *
  • totalTime, avgTimePerRequest - Performance characteristics
  • - *
  • avgRequestsPerSecond - Throughput capacity
  • + *
  • requests, errors, timeouts - Volume and reliability + *
  • totalTime, avgTimePerRequest - Performance characteristics + *
  • avgRequestsPerSecond - Throughput capacity *
- * + * * @param mbeans the raw MBeans response from Solr admin endpoint * @return HandlerStats object containing parsed metrics for all handler types - * * @see HandlerStats * @see HandlerInfo */ @@ -726,62 +739,65 @@ private HandlerStats extractHandlerStats(NamedList mbeans) { if (queryHandlers != null) { // Select handler @SuppressWarnings("unchecked") - NamedList selectHandler = (NamedList) queryHandlers.get(SELECT_HANDLER_PATH); + NamedList selectHandler = + (NamedList) queryHandlers.get(SELECT_HANDLER_PATH); if (selectHandler != null) { @SuppressWarnings("unchecked") NamedList stats = (NamedList) selectHandler.get(STATS_KEY); - selectHandlerInfo = new HandlerInfo( - getLong(stats, REQUESTS_FIELD), - getLong(stats, ERRORS_FIELD), - getLong(stats, TIMEOUTS_FIELD), - getLong(stats, TOTAL_TIME_FIELD), - getFloat(stats, AVG_TIME_PER_REQUEST_FIELD), - getFloat(stats, AVG_REQUESTS_PER_SECOND_FIELD) - ); + selectHandlerInfo = + new HandlerInfo( + getLong(stats, REQUESTS_FIELD), + getLong(stats, ERRORS_FIELD), + getLong(stats, TIMEOUTS_FIELD), + getLong(stats, TOTAL_TIME_FIELD), + getFloat(stats, AVG_TIME_PER_REQUEST_FIELD), + getFloat(stats, AVG_REQUESTS_PER_SECOND_FIELD)); } // Update handler @SuppressWarnings("unchecked") - NamedList updateHandler = (NamedList) queryHandlers.get(UPDATE_HANDLER_PATH); + NamedList updateHandler = + (NamedList) queryHandlers.get(UPDATE_HANDLER_PATH); if (updateHandler != null) { @SuppressWarnings("unchecked") NamedList stats = (NamedList) updateHandler.get(STATS_KEY); - updateHandlerInfo = new HandlerInfo( - getLong(stats, REQUESTS_FIELD), - getLong(stats, ERRORS_FIELD), - getLong(stats, TIMEOUTS_FIELD), - getLong(stats, TOTAL_TIME_FIELD), - getFloat(stats, AVG_TIME_PER_REQUEST_FIELD), - getFloat(stats, AVG_REQUESTS_PER_SECOND_FIELD) - ); + updateHandlerInfo = + new HandlerInfo( + getLong(stats, REQUESTS_FIELD), + getLong(stats, ERRORS_FIELD), + getLong(stats, TIMEOUTS_FIELD), + getLong(stats, TOTAL_TIME_FIELD), + getFloat(stats, AVG_TIME_PER_REQUEST_FIELD), + getFloat(stats, AVG_REQUESTS_PER_SECOND_FIELD)); } } return new HandlerStats(selectHandlerInfo, updateHandlerInfo); } - /** * Extracts the actual collection name from a shard name in SolrCloud environments. - * + * *

In SolrCloud deployments, collection operations often return shard names that include - * replica and shard identifiers (e.g., "films_shard1_replica_n1"). This method extracts - * the base collection name ("films") for use in API calls that require the collection name.

- * - *

Extraction Logic:

+ * replica and shard identifiers (e.g., "films_shard1_replica_n1"). This method extracts the + * base collection name ("films") for use in API calls that require the collection name. + * + *

Extraction Logic: + * *

    - *
  • Detects shard patterns containing the {@value #SHARD_SUFFIX} suffix
  • - *
  • Returns the substring before the shard identifier
  • - *
  • Returns the original string if no shard pattern is detected
  • + *
  • Detects shard patterns containing the {@value #SHARD_SUFFIX} suffix + *
  • Returns the substring before the shard identifier + *
  • Returns the original string if no shard pattern is detected *
- * - *

Examples:

+ * + *

Examples: + * *

    - *
  • "films_shard1_replica_n1" → "films"
  • - *
  • "products_shard2_replica_n3" → "products"
  • - *
  • "simple_collection" → "simple_collection" (unchanged)
  • + *
  • "films_shard1_replica_n1" → "films" + *
  • "products_shard2_replica_n3" → "products" + *
  • "simple_collection" → "simple_collection" (unchanged) *
- * + * * @param collectionOrShard the collection or shard name to parse * @return the extracted collection name, or the original string if no shard pattern found */ @@ -803,27 +819,29 @@ String extractCollectionName(String collectionOrShard) { /** * Validates that a specified collection exists in the Solr cluster. - * - *

Performs collection existence validation by checking against the list of - * available collections. Supports both exact collection name matches and - * shard-based matching for SolrCloud environments.

- * - *

Validation Strategy:

+ * + *

Performs collection existence validation by checking against the list of available + * collections. Supports both exact collection name matches and shard-based matching for + * SolrCloud environments. + * + *

Validation Strategy: + * *

    - *
  1. Exact Match: Checks if the collection name exists exactly
  2. - *
  3. Shard Match: Checks if any shards start with "collection{@value #SHARD_SUFFIX}" pattern
  4. + *
  5. Exact Match: Checks if the collection name exists exactly + *
  6. Shard Match: Checks if any shards start with "collection{@value + * #SHARD_SUFFIX}" pattern *
- * - *

This dual approach ensures compatibility with both standalone Solr - * (which returns core names directly) and SolrCloud (which may return shard names).

- * - *

Error Handling:

- *

Returns {@code false} if validation fails due to communication errors, - * allowing calling methods to handle missing collections appropriately.

- * + * + *

This dual approach ensures compatibility with both standalone Solr (which returns core + * names directly) and SolrCloud (which may return shard names). + * + *

Error Handling: + * + *

Returns {@code false} if validation fails due to communication errors, allowing calling + * methods to handle missing collections appropriately. + * * @param collection the collection name to validate * @return true if the collection exists (either exact or shard match), false otherwise - * * @see #listCollections() * @see #extractCollectionName(String) */ @@ -836,9 +854,10 @@ private boolean validateCollectionExists(String collection) { return true; } - // 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)); + // 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) { @@ -848,48 +867,53 @@ private boolean validateCollectionExists(String collection) { /** * Performs a comprehensive health check on a Solr collection. - * - *

Evaluates collection availability and performance by executing a ping operation - * and basic query to gather health indicators. This method provides a quick way to - * determine if a collection is operational and responding to requests.

- * - *

Health Check Components:

+ * + *

Evaluates collection availability and performance by executing a ping operation and basic + * query to gather health indicators. This method provides a quick way to determine if a + * collection is operational and responding to requests. + * + *

Health Check Components: + * *

    - *
  • Availability: Collection responds to ping requests
  • - *
  • Performance: Response time measurement
  • - *
  • Content: Document count verification using universal query ({@value #ALL_DOCUMENTS_QUERY})
  • - *
  • Timestamp: When the check was performed
  • + *
  • Availability: Collection responds to ping requests + *
  • Performance: Response time measurement + *
  • Content: Document count verification using universal query ({@value + * #ALL_DOCUMENTS_QUERY}) + *
  • Timestamp: When the check was performed *
- * - *

Success Criteria:

- *

A collection is considered healthy if both the ping operation and a basic - * query complete successfully without exceptions. Performance metrics are collected - * during the health check process.

- * - *

Failure Handling:

- *

If the health check fails, a status object is returned with {@code isHealthy=false} - * and the error message describing the failure reason. This allows monitoring - * systems to identify specific issues.

- * - *

MCP Tool Usage:

- *

Exposed as an MCP tool for natural language health queries like - * "check if my_collection is healthy" or "is the search index working properly".

- * + * + *

Success Criteria: + * + *

A collection is considered healthy if both the ping operation and a basic query complete + * successfully without exceptions. Performance metrics are collected during the health check + * process. + * + *

Failure Handling: + * + *

If the health check fails, a status object is returned with {@code isHealthy=false} and + * the error message describing the failure reason. This allows monitoring systems to identify + * specific issues. + * + *

MCP Tool Usage: + * + *

Exposed as an MCP tool for natural language health queries like "check if my_collection is + * healthy" or "is the search index working properly". + * * @param collection the name of the collection to health check * @return SolrHealthStatus object containing health assessment results - * * @see SolrHealthStatus * @see SolrPingResponse */ @McpTool(description = "Check health of a Solr collection") - public SolrHealthStatus checkHealth(@McpToolParam(description = "Solr collection") String collection) { + public SolrHealthStatus checkHealth( + @McpToolParam(description = "Solr collection") String collection) { try { // Ping Solr SolrPingResponse pingResponse = solrClient.ping(collection); // Get basic stats - QueryResponse statsResponse = solrClient.query(collection, - new SolrQuery(ALL_DOCUMENTS_QUERY).setRows(0)); + QueryResponse statsResponse = + solrClient.query(collection, new SolrQuery(ALL_DOCUMENTS_QUERY).setRows(0)); return new SolrHealthStatus( true, @@ -899,21 +923,11 @@ public SolrHealthStatus checkHealth(@McpToolParam(description = "Solr collection new Date(), null, null, - null - ); + null); } catch (Exception e) { return new SolrHealthStatus( - false, - e.getMessage(), - null, - null, - new Date(), - null, - null, - null - ); + false, e.getMessage(), null, null, new Date(), null, null, null); } } - } diff --git a/src/main/java/org/apache/solr/mcp/server/metadata/CollectionUtils.java b/src/main/java/org/apache/solr/mcp/server/metadata/CollectionUtils.java index bdcb0f9..fe3c621 100644 --- a/src/main/java/org/apache/solr/mcp/server/metadata/CollectionUtils.java +++ b/src/main/java/org/apache/solr/mcp/server/metadata/CollectionUtils.java @@ -19,35 +19,38 @@ import org.apache.solr.common.util.NamedList; /** - * Utility class providing type-safe helper methods for extracting values from Apache Solr NamedList objects. - * - *

This utility class simplifies the process of working with Solr's {@code NamedList} response format - * by providing robust type conversion methods that handle various data formats and edge cases commonly - * encountered when processing Solr admin and query responses.

- * - *

Key Benefits:

+ * Utility class providing type-safe helper methods for extracting values from Apache Solr NamedList + * objects. + * + *

This utility class simplifies the process of working with Solr's {@code NamedList} response + * format by providing robust type conversion methods that handle various data formats and edge + * cases commonly encountered when processing Solr admin and query responses. + * + *

Key Benefits: + * *

    - *
  • Type Safety: Automatic conversion with proper error handling
  • - *
  • Null Safety: Graceful handling of missing or null values
  • - *
  • Format Flexibility: Support for multiple input data types
  • - *
  • Error Resilience: Defensive programming against malformed data
  • + *
  • Type Safety: Automatic conversion with proper error handling + *
  • Null Safety: Graceful handling of missing or null values + *
  • Format Flexibility: Support for multiple input data types + *
  • Error Resilience: Defensive programming against malformed data *
- * - *

Common Use Cases:

+ * + *

Common Use Cases: + * *

    - *
  • Extracting metrics from Solr MBeans responses
  • - *
  • Processing Luke handler index statistics
  • - *
  • Converting admin API response values to typed objects
  • - *
  • Handling cache and handler performance metrics
  • + *
  • Extracting metrics from Solr MBeans responses + *
  • Processing Luke handler index statistics + *
  • Converting admin API response values to typed objects + *
  • Handling cache and handler performance metrics *
- * - *

Thread Safety:

- *

All methods in this utility class are stateless and thread-safe, making them - * suitable for use in concurrent environments and Spring service beans.

+ * + *

Thread Safety: + * + *

All methods in this utility class are stateless and thread-safe, making them suitable for use + * in concurrent environments and Spring service beans. * * @version 0.0.1 * @since 0.0.1 - * * @see org.apache.solr.common.util.NamedList * @see CollectionService */ @@ -55,33 +58,35 @@ public class CollectionUtils { /** * Extracts a Long value from a NamedList using the specified key with robust type conversion. - * + * *

This method provides flexible extraction of Long values from Solr NamedList responses, * handling various input formats that may be returned by different Solr endpoints. It performs - * safe type conversion with appropriate error handling for malformed data.

- * - *

Supported Input Types:

+ * safe type conversion with appropriate error handling for malformed data. + * + *

Supported Input Types: + * *

    - *
  • Number instances: Integer, Long, Double, Float, BigInteger, BigDecimal
  • - *
  • String representations: Numeric strings that can be parsed as Long
  • - *
  • Null values: Returns null without throwing exceptions
  • + *
  • Number instances: Integer, Long, Double, Float, BigInteger, BigDecimal + *
  • String representations: Numeric strings that can be parsed as Long + *
  • Null values: Returns null without throwing exceptions *
- * - *

Error Handling:

+ * + *

Error Handling: + * *

Returns {@code null} for missing keys, null values, or unparseable strings rather than - * throwing exceptions, enabling graceful degradation in metrics collection scenarios.

- * - *

Common Use Cases:

+ * throwing exceptions, enabling graceful degradation in metrics collection scenarios. + * + *

Common Use Cases: + * *

    - *
  • Cache statistics: hits, lookups, evictions, size
  • - *
  • Handler metrics: request counts, error counts, timeouts
  • - *
  • Index statistics: document counts, segment information
  • + *
  • Cache statistics: hits, lookups, evictions, size + *
  • Handler metrics: request counts, error counts, timeouts + *
  • Index statistics: document counts, segment information *
* * @param response the NamedList containing the data to extract from * @param key the key to look up in the NamedList * @return the Long value if found and convertible, null otherwise - * * @see Number#longValue() * @see Long#parseLong(String) */ @@ -101,37 +106,42 @@ public static Long getLong(NamedList response, String key) { } /** - * Extracts a Float value from a NamedList using the specified key with automatic type conversion. - * + * Extracts a Float value from a NamedList using the specified key with automatic type + * conversion. + * *

This method provides convenient extraction of Float values from Solr NamedList responses, * commonly used for extracting percentage values, ratios, and performance metrics. It assumes - * that missing values should be treated as zero, which is appropriate for most metric scenarios.

- * - *

Type Conversion:

+ * that missing values should be treated as zero, which is appropriate for most metric + * scenarios. + * + *

Type Conversion: + * *

Automatically converts any Number instance to Float using the {@link Number#floatValue()} - * method, ensuring compatibility with various numeric types returned by Solr.

- * - *

Default Value Behavior:

+ * method, ensuring compatibility with various numeric types returned by Solr. + * + *

Default Value Behavior: + * *

Returns {@code 0.0f} for missing or null values, which is typically the desired behavior - * for metrics like hit ratios, performance averages, and statistical calculations where - * missing data should be interpreted as zero.

- * - *

Common Use Cases:

+ * for metrics like hit ratios, performance averages, and statistical calculations where missing + * data should be interpreted as zero. + * + *

Common Use Cases: + * *

    - *
  • Cache hit ratios and performance percentages
  • - *
  • Average response times and throughput metrics
  • - *
  • Statistical calculations and performance indicators
  • + *
  • Cache hit ratios and performance percentages + *
  • Average response times and throughput metrics + *
  • Statistical calculations and performance indicators *
- * - *

Note:

- *

This method differs from {@link #getLong(NamedList, String)} by returning a default - * value instead of null, which is more appropriate for Float metrics that represent - * rates, ratios, or averages.

+ * + *

Note: + * + *

This method differs from {@link #getLong(NamedList, String)} by returning a default value + * instead of null, which is more appropriate for Float metrics that represent rates, ratios, or + * averages. * * @param stats the NamedList containing the metric data to extract from * @param key the key to look up in the NamedList * @return the Float value if found, or 0.0f if the key doesn't exist or value is null - * * @see Number#floatValue() */ public static Float getFloat(NamedList stats, String key) { @@ -140,43 +150,48 @@ public static Float getFloat(NamedList stats, String key) { } /** - * Extracts an Integer value from a NamedList using the specified key with robust type conversion. - * + * Extracts an Integer value from a NamedList using the specified key with robust type + * conversion. + * *

This method provides flexible extraction of Integer values from Solr NamedList responses, * handling various input formats that may be returned by different Solr endpoints. It performs - * safe type conversion with appropriate error handling for malformed data.

- * - *

Supported Input Types:

+ * safe type conversion with appropriate error handling for malformed data. + * + *

Supported Input Types: + * *

    - *
  • Number instances: Integer, Long, Double, Float, BigInteger, BigDecimal
  • - *
  • String representations: Numeric strings that can be parsed as Integer
  • - *
  • Null values: Returns null without throwing exceptions
  • + *
  • Number instances: Integer, Long, Double, Float, BigInteger, BigDecimal + *
  • String representations: Numeric strings that can be parsed as Integer + *
  • Null values: Returns null without throwing exceptions *
- * - *

Type Conversion Strategy:

- *

For Number instances, uses {@link Number#intValue()} which truncates decimal values. - * For string values, attempts parsing with {@link Integer#parseInt(String)} and returns - * null if parsing fails rather than throwing an exception.

- * - *

Error Handling:

+ * + *

Type Conversion Strategy: + * + *

For Number instances, uses {@link Number#intValue()} which truncates decimal values. For + * string values, attempts parsing with {@link Integer#parseInt(String)} and returns null if + * parsing fails rather than throwing an exception. + * + *

Error Handling: + * *

Returns {@code null} for missing keys, null values, or unparseable strings rather than - * throwing exceptions, enabling graceful degradation in metrics collection scenarios.

- * - *

Common Use Cases:

+ * throwing exceptions, enabling graceful degradation in metrics collection scenarios. + * + *

Common Use Cases: + * *

    - *
  • Index segment counts and document counts (when within Integer range)
  • - *
  • Configuration values and small numeric metrics
  • - *
  • Count-based statistics that don't exceed Integer.MAX_VALUE
  • + *
  • Index segment counts and document counts (when within Integer range) + *
  • Configuration values and small numeric metrics + *
  • Count-based statistics that don't exceed Integer.MAX_VALUE *
- * - *

Range Considerations:

- *

For large values that may exceed Integer range, consider using {@link #getLong(NamedList, String)} - * instead to avoid truncation or overflow issues.

+ * + *

Range Considerations: + * + *

For large values that may exceed Integer range, consider using {@link #getLong(NamedList, + * String)} instead to avoid truncation or overflow issues. * * @param response the NamedList containing the data to extract from * @param key the key to look up in the NamedList * @return the Integer value if found and convertible, null otherwise - * * @see Number#intValue() * @see Integer#parseInt(String) * @see #getLong(NamedList, String) @@ -195,5 +210,4 @@ public static Integer getInteger(NamedList response, String key) { return null; } } - } diff --git a/src/main/java/org/apache/solr/mcp/server/metadata/Dtos.java b/src/main/java/org/apache/solr/mcp/server/metadata/Dtos.java index b941391..7d1fbf7 100644 --- a/src/main/java/org/apache/solr/mcp/server/metadata/Dtos.java +++ b/src/main/java/org/apache/solr/mcp/server/metadata/Dtos.java @@ -19,22 +19,23 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - import java.util.Date; /** * 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.

+ *

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: * - *

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
  • + *
  • 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 *
* * @version 0.0.1 @@ -43,29 +44,31 @@ /** * Top-level container for comprehensive Solr collection metrics. - * - *

This class aggregates various types of Solr performance and operational metrics - * including index statistics, query performance, cache utilization, and request handler metrics. - * It serves as the primary response object for collection monitoring and analysis tools.

- * - *

The metrics are collected from multiple Solr admin endpoints and MBeans to provide - * a comprehensive view of collection health and performance characteristics.

- * - *

Null-Safe Design:

+ * + *

This class aggregates various types of Solr performance and operational metrics including + * index statistics, query performance, cache utilization, and request handler metrics. It serves as + * the primary response object for collection monitoring and analysis tools. + * + *

The metrics are collected from multiple Solr admin endpoints and MBeans to provide a + * comprehensive view of collection health and performance characteristics. + * + *

Null-Safe Design: + * *

Individual metric components (cache stats, handler stats) may be null if the corresponding - * data is unavailable or empty. Always check for null values before accessing nested properties.

- * - *

Example usage:

+ * data is unavailable or empty. Always check for null values before accessing nested properties. + * + *

Example usage: + * *

{@code
  * SolrMetrics metrics = collectionService.getCollectionStats("my_collection");
  * System.out.println("Documents: " + metrics.getIndexStats().getNumDocs());
- * 
+ *
  * // Safe null checking for optional metrics
  * if (metrics.getCacheStats() != null && metrics.getCacheStats().getQueryResultCache() != null) {
  *     System.out.println("Cache hit ratio: " + metrics.getCacheStats().getQueryResultCache().getHitratio());
  * }
  * }
- * + * * @see IndexStats * @see QueryStats * @see CacheStats @@ -74,299 +77,299 @@ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record SolrMetrics( - /** Index-related statistics including document counts and segment information */ - IndexStats indexStats, + /** Index-related statistics including document counts and segment information */ + IndexStats indexStats, - /** Query performance metrics from the most recent search operations */ - QueryStats queryStats, + /** Query performance metrics from the most recent search operations */ + QueryStats queryStats, - /** Cache utilization statistics for query result, document, and filter caches (may be null) */ - CacheStats cacheStats, + /** + * Cache utilization statistics for query result, document, and filter caches (may be null) + */ + CacheStats cacheStats, - /** Request handler performance metrics for select and update operations (may be null) */ - HandlerStats handlerStats, + /** Request handler performance metrics for select and update operations (may be null) */ + HandlerStats handlerStats, - /** Timestamp when these metrics were collected, formatted as ISO 8601 */ - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - Date timestamp -) { -} + /** Timestamp when these metrics were collected, formatted as ISO 8601 */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + Date timestamp) {} /** * Lucene index statistics for a Solr collection. - * - *

Provides essential information about the underlying Lucene index structure - * and document composition. These metrics are retrieved using Solr's Luke request handler - * which exposes Lucene-level index information.

- * - *

Available Metrics:

+ * + *

Provides essential information about the underlying Lucene index structure and document + * composition. These metrics are retrieved using Solr's Luke request handler which exposes + * Lucene-level index information. + * + *

Available Metrics: + * *

    - *
  • numDocs: Total number of documents excluding deleted documents
  • - *
  • segmentCount: Number of Lucene segments (affects search performance)
  • + *
  • numDocs: Total number of documents excluding deleted documents + *
  • segmentCount: Number of Lucene segments (affects search performance) *
- * - *

Performance Implications:

- *

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.

- * + * + *

Performance Implications: + * + *

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. + * * @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) */ - Integer numDocs, + /** Total number of documents in the index (excluding deleted documents) */ + Integer numDocs, - /** Number of Lucene segments in the index (lower numbers generally indicate better performance) */ - Integer segmentCount -) { -} + /** + * Number of Lucene segments in the index (lower numbers generally indicate better + * performance) + */ + Integer segmentCount) {} /** * Field-level statistics for individual Solr schema fields. - * - *

Provides detailed information about how individual fields are utilized within - * the Solr index. This information helps with schema optimization and understanding - * field usage patterns.

- * - *

Statistics include:

+ * + *

Provides detailed information about how individual fields are utilized within the Solr index. + * This information helps with schema optimization and understanding field usage patterns. + * + *

Statistics include: + * *

    - *
  • type: Solr field type (e.g., "text_general", "int", "date")
  • - *
  • docs: Number of documents containing this field
  • - *
  • distinct: Number of unique values for this field
  • + *
  • type: Solr field type (e.g., "text_general", "int", "date") + *
  • docs: Number of documents containing this field + *
  • distinct: Number of unique values for this field *
- * - *

Analysis Insights:

- *

High cardinality fields (high distinct values) may require special indexing - * considerations, while sparsely populated fields (low docs count) might benefit - * from different storage strategies.

- * - *

Note: This class is currently unused in the collection statistics - * but is available for future field-level analysis features.

+ * + *

Analysis Insights: + * + *

High cardinality fields (high distinct values) may require special indexing considerations, + * while sparsely populated fields (low docs count) might benefit from different storage strategies. + * + *

Note: This class is currently unused in the collection statistics but is + * available for future field-level analysis features. */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record FieldStats( - /** Solr field type as defined in the schema configuration */ - String type, + /** Solr field type as defined in the schema configuration */ + String type, - /** Number of documents in the index that contain this field */ - Integer docs, + /** Number of documents in the index that contain this field */ + Integer docs, - /** Number of unique/distinct values for this field across all documents */ - Integer distinct -) { -} + /** Number of unique/distinct values for this field across all documents */ + Integer distinct) {} /** * Query execution performance metrics from Solr search operations. - * - *

Captures performance characteristics and result metadata from the most recent - * query execution. These metrics help identify query performance patterns and - * potential optimization opportunities.

- * - *

Available Metrics:

+ * + *

Captures performance characteristics and result metadata from the most recent query execution. + * These metrics help identify query performance patterns and potential optimization opportunities. + * + *

Available Metrics: + * *

    - *
  • queryTime: Time in milliseconds to execute the query
  • - *
  • totalResults: Total number of matching documents found
  • - *
  • start: Starting offset for pagination
  • - *
  • maxScore: Highest relevance score in the result set
  • + *
  • queryTime: Time in milliseconds to execute the query + *
  • totalResults: Total number of matching documents found + *
  • start: Starting offset for pagination + *
  • maxScore: Highest relevance score in the result set *
- * - *

Performance Analysis:

- *

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.

- * + * + *

Performance Analysis: + * + *

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. + * * @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, + /** Time in milliseconds required to execute the most recent query */ + Integer queryTime, - /** Total number of documents matching the query criteria */ - Long totalResults, + /** Total number of documents matching the query criteria */ + Long totalResults, - /** Starting position for paginated results (0-based offset) */ - Long start, + /** Starting position for paginated results (0-based offset) */ + Long start, - /** Highest relevance score among the returned documents */ - Float maxScore -) { -} + /** Highest relevance score among the returned documents */ + Float maxScore) {} /** * Solr cache utilization statistics across all cache types. - * - *

Aggregates cache performance metrics for the three primary Solr caches. - * Cache performance directly impacts query response times and system resource - * utilization, making these metrics critical for performance tuning.

- * - *

Monitored Cache Types:

+ * + *

Aggregates cache performance metrics for the three primary Solr caches. Cache performance + * directly impacts query response times and system resource utilization, making these metrics + * critical for performance tuning. + * + *

Monitored Cache Types: + * *

    - *
  • queryResultCache: Caches complete query results
  • - *
  • documentCache: Caches retrieved document data
  • - *
  • filterCache: Caches filter query results
  • + *
  • queryResultCache: Caches complete query results + *
  • documentCache: Caches retrieved document data + *
  • filterCache: Caches filter query results *
- * - *

Cache Analysis:

- *

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.

- * + * + *

Cache Analysis: + * + *

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. + * * @see CacheInfo */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record CacheStats( - /** Performance metrics for the query result cache */ - CacheInfo queryResultCache, + /** Performance metrics for the query result cache */ + CacheInfo queryResultCache, - /** Performance metrics for the document cache */ - CacheInfo documentCache, + /** Performance metrics for the document cache */ + CacheInfo documentCache, - /** Performance metrics for the filter cache */ - CacheInfo filterCache -) { -} + /** Performance metrics for the filter cache */ + CacheInfo filterCache) {} /** * Detailed performance metrics for individual Solr cache instances. - * - *

Provides comprehensive cache utilization statistics including hit ratios, - * eviction rates, and current size metrics. These metrics are essential for - * cache tuning and memory management optimization.

- * - *

Key Performance Indicators:

+ * + *

Provides comprehensive cache utilization statistics including hit ratios, eviction rates, and + * current size metrics. These metrics are essential for cache tuning and memory management + * optimization. + * + *

Key Performance Indicators: + * *

    - *
  • hitratio: Cache effectiveness (higher is better)
  • - *
  • evictions: Memory pressure indicator
  • - *
  • size: Current cache utilization
  • - *
  • lookups vs hits: Cache request patterns
  • + *
  • hitratio: Cache effectiveness (higher is better) + *
  • evictions: Memory pressure indicator + *
  • size: Current cache utilization + *
  • lookups vs hits: Cache request patterns *
- * - *

Performance Targets:

- *

Optimal cache performance typically shows high hit ratios (>0.80) with - * minimal evictions. High eviction rates suggest cache size increases may - * improve performance.

+ * + *

Performance Targets: + * + *

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

Tracks performance metrics for the primary Solr request handlers that process - * search and update operations. Handler performance directly affects user experience - * and system throughput capacity.

- * - *

Monitored Handlers:

+ * + *

Tracks performance metrics for the primary Solr request handlers that process search and + * update operations. Handler performance directly affects user experience and system throughput + * capacity. + * + *

Monitored Handlers: + * *

    - *
  • selectHandler: Processes search/query requests (/select)
  • - *
  • updateHandler: Processes document indexing requests (/update)
  • + *
  • selectHandler: Processes search/query requests (/select) + *
  • updateHandler: Processes document indexing requests (/update) *
- * - *

Performance Analysis:

- *

Handler metrics help identify bottlenecks in request processing and guide - * capacity planning decisions. High error rates or response times indicate - * potential optimization needs.

- * + * + *

Performance Analysis: + * + *

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

Provides comprehensive request handler statistics including throughput, - * error rates, and performance characteristics. These metrics are crucial for - * identifying performance bottlenecks and system reliability issues.

- * - *

Performance Metrics:

+ * + *

Provides comprehensive request handler statistics including throughput, error rates, and + * performance characteristics. These metrics are crucial for identifying performance bottlenecks + * and system reliability issues. + * + *

Performance Metrics: + * *

    - *
  • requests: Total volume processed
  • - *
  • errors: Reliability indicator
  • - *
  • avgTimePerRequest: Response time performance
  • - *
  • avgRequestsPerSecond: Throughput capacity
  • + *
  • requests: Total volume processed + *
  • errors: Reliability indicator + *
  • avgTimePerRequest: Response time performance + *
  • avgRequestsPerSecond: Throughput capacity *
- * - *

Health Indicators:

- *

High error rates may indicate system stress or configuration issues. - * Increasing response times suggest capacity limits or optimization needs.

+ * + *

Health Indicators: + * + *

High error rates may indicate system stress or configuration issues. Increasing response times + * suggest capacity limits or optimization needs. */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) record HandlerInfo( - /** Total number of requests processed by this handler */ - Long requests, + /** Total number of requests processed by this handler */ + Long requests, - /** Number of requests that resulted in errors */ - Long errors, + /** Number of requests that resulted in errors */ + Long errors, - /** Number of requests that exceeded timeout limits */ - Long timeouts, + /** Number of requests that exceeded timeout limits */ + Long timeouts, - /** Cumulative time spent processing all requests (milliseconds) */ - Long totalTime, + /** Cumulative time spent processing all requests (milliseconds) */ + Long totalTime, - /** Average time per request in milliseconds */ - Float avgTimePerRequest, + /** Average time per request in milliseconds */ + Float avgTimePerRequest, - /** Average throughput in requests per second */ - Float avgRequestsPerSecond -) { -} + /** Average throughput in requests per second */ + Float avgRequestsPerSecond) {} /** * Comprehensive health status assessment for Solr collections. - * - *

Provides a complete health check result including availability status, - * performance metrics, and diagnostic information. This serves as a primary - * monitoring endpoint for collection operational status.

- * - *

Health Assessment Components:

+ * + *

Provides a complete health check result including availability status, performance metrics, + * and diagnostic information. This serves as a primary monitoring endpoint for collection + * operational status. + * + *

Health Assessment Components: + * *

    - *
  • isHealthy: Overall collection availability
  • - *
  • responseTime: Performance indicator
  • - *
  • totalDocuments: Content availability
  • - *
  • errorMessage: Diagnostic information when unhealthy
  • + *
  • isHealthy: Overall collection availability + *
  • responseTime: Performance indicator + *
  • totalDocuments: Content availability + *
  • errorMessage: Diagnostic information when unhealthy *
- * - *

Monitoring Integration:

- *

This DTO is typically used by monitoring systems and dashboards to provide - * real-time collection health status and enable automated alerting on failures.

- * - *

Example usage:

+ * + *

Monitoring Integration: + * + *

This DTO is typically used by monitoring systems and dashboards to provide real-time + * collection health status and enable automated alerting on failures. + * + *

Example usage: + * *

{@code
  * SolrHealthStatus status = collectionService.checkHealth("my_collection");
  * if (!status.isHealthy()) {
@@ -377,29 +380,27 @@ record HandlerInfo(
 @JsonIgnoreProperties(ignoreUnknown = true)
 @JsonInclude(JsonInclude.Include.NON_NULL)
 record SolrHealthStatus(
-    /** Overall health status - true if collection is operational and responding */
-    boolean isHealthy,
+        /** Overall health status - true if collection is operational and responding */
+        boolean isHealthy,
 
-    /** Detailed error message when isHealthy is false, null when healthy */
-    String errorMessage,
+        /** Detailed error message when isHealthy is false, null when healthy */
+        String errorMessage,
 
-    /** Response time in milliseconds for the health check ping request */
-    Long responseTime,
+        /** Response time in milliseconds for the health check ping request */
+        Long responseTime,
 
-    /** Total number of documents currently indexed in the collection */
-    Long totalDocuments,
+        /** Total number of documents currently indexed in the collection */
+        Long totalDocuments,
 
-    /** Timestamp when this health check was performed, formatted as ISO 8601 */
-    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
-    Date lastChecked,
+        /** Timestamp when this health check was performed, formatted as ISO 8601 */
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
+                Date lastChecked,
 
-    /** Name of the collection that was checked */
-    String collection,
+        /** Name of the collection that was checked */
+        String collection,
 
-    /** Version of Solr server (when available) */
-    String solrVersion,
+        /** Version of Solr server (when available) */
+        String solrVersion,
 
-    /** Additional status information or state description */
-    String status
-) {
-}
\ No newline at end of file
+        /** Additional status information or state description */
+        String status) {}
diff --git a/src/main/java/org/apache/solr/mcp/server/metadata/SchemaService.java b/src/main/java/org/apache/solr/mcp/server/metadata/SchemaService.java
index 14c8262..72ac9b0 100644
--- a/src/main/java/org/apache/solr/mcp/server/metadata/SchemaService.java
+++ b/src/main/java/org/apache/solr/mcp/server/metadata/SchemaService.java
@@ -23,56 +23,64 @@
 import org.springframework.stereotype.Service;
 
 /**
- * Spring Service providing schema introspection and management capabilities for Apache Solr collections.
- * 
- * 

This service enables exploration and analysis of Solr collection schemas through the Model Context - * Protocol (MCP), allowing AI clients to understand field definitions, data types, and schema configuration - * for intelligent query construction and data analysis workflows.

- * - *

Core Capabilities:

+ * Spring Service providing schema introspection and management capabilities for Apache Solr + * collections. + * + *

This service enables exploration and analysis of Solr collection schemas through the Model + * Context Protocol (MCP), allowing AI clients to understand field definitions, data types, and + * schema configuration for intelligent query construction and data analysis workflows. + * + *

Core Capabilities: + * *

    - *
  • Schema Retrieval: Complete schema information for any collection
  • - *
  • Field Introspection: Detailed field type and configuration analysis
  • - *
  • Dynamic Field Support: Discovery of dynamic field patterns and rules
  • - *
  • Copy Field Analysis: Understanding of field copying and aggregation rules
  • + *
  • Schema Retrieval: Complete schema information for any collection + *
  • Field Introspection: Detailed field type and configuration analysis + *
  • Dynamic Field Support: Discovery of dynamic field patterns and rules + *
  • Copy Field Analysis: Understanding of field copying and aggregation rules *
- * - *

Schema Information Provided:

+ * + *

Schema Information Provided: + * *

    - *
  • Field Definitions: Names, types, indexing, and storage configurations
  • - *
  • Field Types: Analyzer configurations, tokenization, and filtering rules
  • - *
  • Dynamic Fields: Pattern-based field matching and type assignment
  • - *
  • Copy Fields: Source-to-destination field copying configurations
  • - *
  • Unique Key: Primary key field identification and configuration
  • + *
  • Field Definitions: Names, types, indexing, and storage configurations + *
  • Field Types: Analyzer configurations, tokenization, and filtering rules + *
  • Dynamic Fields: Pattern-based field matching and type assignment + *
  • Copy Fields: Source-to-destination field copying configurations + *
  • Unique Key: Primary key field identification and configuration *
- * - *

MCP Tool Integration:

- *

Schema operations are exposed as MCP tools that AI clients can invoke through natural - * language requests such as "show me the schema for my_collection" or "what fields are - * available for searching in the products index".

- * - *

Use Cases:

+ * + *

MCP Tool Integration: + * + *

Schema operations are exposed as MCP tools that AI clients can invoke through natural language + * requests such as "show me the schema for my_collection" or "what fields are available for + * searching in the products index". + * + *

Use Cases: + * *

    - *
  • Query Planning: Understanding available fields for search construction
  • - *
  • Data Analysis: Identifying field types and capabilities for analytics
  • - *
  • Index Optimization: Analyzing field configurations for performance tuning
  • - *
  • Schema Documentation: Generating documentation from live schema definitions
  • + *
  • Query Planning: Understanding available fields for search construction + *
  • Data Analysis: Identifying field types and capabilities for analytics + *
  • Index Optimization: Analyzing field configurations for performance tuning + *
  • Schema Documentation: Generating documentation from live schema + * definitions *
- * - *

Integration with Other Services:

- *

Schema information complements other MCP services by providing the metadata necessary - * for intelligent search query construction, field validation, and result interpretation.

- * - *

Example Usage:

+ * + *

Integration with Other Services: + * + *

Schema information complements other MCP services by providing the metadata necessary for + * intelligent search query construction, field validation, and result interpretation. + * + *

Example Usage: + * *

{@code
  * // Get complete schema information
  * SchemaRepresentation schema = schemaService.getSchema("products");
- * 
+ *
  * // Analyze field configurations
  * schema.getFields().forEach(field -> {
  *     System.out.println("Field: " + field.getName() + " Type: " + field.getType());
  * });
- * 
+ *
  * // Examine dynamic field patterns
  * schema.getDynamicFields().forEach(dynField -> {
  *     System.out.println("Pattern: " + dynField.getName() + " Type: " + dynField.getType());
@@ -81,7 +89,6 @@
  *
  * @version 0.0.1
  * @since 0.0.1
- * 
  * @see SchemaRepresentation
  * @see org.apache.solr.client.solrj.request.schema.SchemaRequest
  * @see org.springframework.ai.tool.annotation.Tool
@@ -94,13 +101,12 @@ public class SchemaService {
 
     /**
      * Constructs a new SchemaService with the required SolrClient dependency.
-     * 
-     * 

This constructor is automatically called by Spring's dependency injection - * framework during application startup, providing the service with the necessary - * Solr client for schema operations.

- * + * + *

This constructor is automatically called by Spring's dependency injection framework during + * application startup, providing the service with the necessary Solr client for schema + * operations. + * * @param solrClient the SolrJ client instance for communicating with Solr - * * @see SolrClient */ public SchemaService(SolrClient solrClient) { @@ -109,56 +115,61 @@ public SchemaService(SolrClient solrClient) { /** * Retrieves the complete schema definition for a specified Solr collection. - * - *

This method provides comprehensive access to all schema components including - * field definitions, field types, dynamic fields, copy fields, and schema-level - * configuration. The returned schema representation contains all information - * necessary for understanding the collection's data structure and capabilities.

- * - *

Schema Components Included:

+ * + *

This method provides comprehensive access to all schema components including field + * definitions, field types, dynamic fields, copy fields, and schema-level configuration. The + * returned schema representation contains all information necessary for understanding the + * collection's data structure and capabilities. + * + *

Schema Components Included: + * *

    - *
  • Fields: Static field definitions with types and properties
  • - *
  • Field Types: Analyzer configurations and processing rules
  • - *
  • Dynamic Fields: Pattern-based field matching rules
  • - *
  • Copy Fields: Field copying and aggregation configurations
  • - *
  • Unique Key: Primary key field specification
  • - *
  • Schema Attributes: Version, name, and global settings
  • + *
  • Fields: Static field definitions with types and properties + *
  • Field Types: Analyzer configurations and processing rules + *
  • Dynamic Fields: Pattern-based field matching rules + *
  • Copy Fields: Field copying and aggregation configurations + *
  • Unique Key: Primary key field specification + *
  • Schema Attributes: Version, name, and global settings *
- * - *

Field Information Details:

- *

Each field definition includes comprehensive metadata:

+ * + *

Field Information Details: + * + *

Each field definition includes comprehensive metadata: + * *

    - *
  • Name: Field identifier for queries and indexing
  • - *
  • Type: Reference to field type configuration
  • - *
  • Indexed: Whether the field is searchable
  • - *
  • Stored: Whether field values are retrievable
  • - *
  • Multi-valued: Whether multiple values are allowed
  • - *
  • Required: Whether the field must have a value
  • + *
  • Name: Field identifier for queries and indexing + *
  • Type: Reference to field type configuration + *
  • Indexed: Whether the field is searchable + *
  • Stored: Whether field values are retrievable + *
  • Multi-valued: Whether multiple values are allowed + *
  • Required: Whether the field must have a value *
- * - *

MCP Tool Usage:

- *

AI clients can invoke this method with natural language requests such as:

+ * + *

MCP Tool Usage: + * + *

AI clients can invoke this method with natural language requests such as: + * *

    - *
  • "Show me the schema for the products collection"
  • - *
  • "What fields are available in my_index?"
  • - *
  • "Get the field definitions for the search index"
  • + *
  • "Show me the schema for the products collection" + *
  • "What fields are available in my_index?" + *
  • "Get the field definitions for the search index" *
- * - *

Error Handling:

- *

If the collection does not exist or schema retrieval fails, the method - * will throw an exception with details about the failure reason. Common issues - * include collection name typos, permission problems, or Solr connectivity issues.

- * - *

Performance Considerations:

- *

Schema information is typically cached by Solr and retrieval is generally - * fast. However, for applications that frequently access schema information, - * consider implementing client-side caching to reduce network overhead.

- * + * + *

Error Handling: + * + *

If the collection does not exist or schema retrieval fails, the method will throw an + * exception with details about the failure reason. Common issues include collection name typos, + * permission problems, or Solr connectivity issues. + * + *

Performance Considerations: + * + *

Schema information is typically cached by Solr and retrieval is generally fast. However, + * for applications that frequently access schema information, consider implementing client-side + * caching to reduce network overhead. + * * @param collection the name of the Solr collection to retrieve schema information for * @return complete schema representation containing all field and type definitions - * * @throws Exception if collection does not exist, access is denied, or communication fails - * * @see SchemaRepresentation * @see SchemaRequest * @see org.apache.solr.client.solrj.response.schema.SchemaResponse @@ -168,5 +179,4 @@ public SchemaRepresentation getSchema(String collection) throws Exception { SchemaRequest schemaRequest = new SchemaRequest(); return schemaRequest.process(solrClient, collection).getSchemaRepresentation(); } - -} \ No newline at end of file +} diff --git a/src/main/java/org/apache/solr/mcp/server/package-info.java b/src/main/java/org/apache/solr/mcp/server/package-info.java index 7631e3a..0ff54ed 100644 --- a/src/main/java/org/apache/solr/mcp/server/package-info.java +++ b/src/main/java/org/apache/solr/mcp/server/package-info.java @@ -17,4 +17,4 @@ @NullMarked package org.apache.solr.mcp.server; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/apache/solr/mcp/server/search/SearchResponse.java b/src/main/java/org/apache/solr/mcp/server/search/SearchResponse.java index cee55b3..3dc4e03 100644 --- a/src/main/java/org/apache/solr/mcp/server/search/SearchResponse.java +++ b/src/main/java/org/apache/solr/mcp/server/search/SearchResponse.java @@ -21,60 +21,67 @@ /** * Immutable record representing a structured search response from Apache Solr operations. - * - *

This record encapsulates all essential components of a Solr search result in a - * type-safe, immutable structure that can be easily serialized to JSON for MCP client - * consumption. It provides a clean abstraction over Solr's native response format while - * preserving all critical search metadata and result data.

- * - *

Record Benefits:

+ * + *

This record encapsulates all essential components of a Solr search result in a type-safe, + * immutable structure that can be easily serialized to JSON for MCP client consumption. It provides + * a clean abstraction over Solr's native response format while preserving all critical search + * metadata and result data. + * + *

Record Benefits: + * *

    - *
  • Immutability: Response data cannot be modified after creation
  • - *
  • Type Safety: Compile-time validation of response structure
  • - *
  • JSON Serialization: Automatic conversion to JSON for MCP clients
  • - *
  • Memory Efficiency: Compact representation with minimal overhead
  • + *
  • Immutability: Response data cannot be modified after creation + *
  • Type Safety: Compile-time validation of response structure + *
  • JSON Serialization: Automatic conversion to JSON for MCP clients + *
  • Memory Efficiency: Compact representation with minimal overhead *
- * - *

Search Metadata:

- *

The response includes comprehensive search metadata that helps clients understand - * the query results and implement pagination, relevance analysis, and user interfaces:

+ * + *

Search Metadata: + * + *

The response includes comprehensive search metadata that helps clients understand the query + * results and implement pagination, relevance analysis, and user interfaces: + * *

    - *
  • Total Results: Complete count of matching documents
  • - *
  • Pagination Info: Current offset for result windowing
  • - *
  • Relevance Scoring: Maximum relevance score in the result set
  • + *
  • Total Results: Complete count of matching documents + *
  • Pagination Info: Current offset for result windowing + *
  • Relevance Scoring: Maximum relevance score in the result set *
- * - *

Document Structure:

- *

Documents are represented as flexible key-value maps to accommodate Solr's - * dynamic field capabilities and schema-less operation. Each document map contains - * field names as keys and field values as objects, preserving the original data types - * from Solr (strings, numbers, dates, arrays, etc.).

- * - *

Faceting Support:

- *

Facet information is structured as a nested map hierarchy where the outer map - * represents facet field names and inner maps contain facet values with their - * corresponding document counts. This structure efficiently supports multiple - * faceting strategies including field faceting and range faceting.

- * - *

Usage Examples:

+ * + *

Document Structure: + * + *

Documents are represented as flexible key-value maps to accommodate Solr's dynamic field + * capabilities and schema-less operation. Each document map contains field names as keys and field + * values as objects, preserving the original data types from Solr (strings, numbers, dates, arrays, + * etc.). + * + *

Faceting Support: + * + *

Facet information is structured as a nested map hierarchy where the outer map represents facet + * field names and inner maps contain facet values with their corresponding document counts. This + * structure efficiently supports multiple faceting strategies including field faceting and range + * faceting. + * + *

Usage Examples: + * *

{@code
  * // Access search results
  * SearchResponse response = searchService.search("products", "laptop", null, null, null, 0, 10);
  * System.out.println("Found " + response.numFound() + " products");
- * 
+ *
  * // Iterate through documents
  * for (Map doc : response.documents()) {
  *     System.out.println("Title: " + doc.get("title"));
  *     System.out.println("Price: " + doc.get("price"));
  * }
- * 
+ *
  * // Access facet data
  * Map categoryFacets = response.facets().get("category");
- * categoryFacets.forEach((category, count) -> 
+ * categoryFacets.forEach((category, count) ->
  *     System.out.println(category + ": " + count + " items"));
  * }
- * - *

JSON Serialization Example:

+ * + *

JSON Serialization Example: + * *

{@code
  * {
  *   "numFound": 150,
@@ -90,16 +97,14 @@
  *   }
  * }
  * }
- * + * * @param numFound total number of documents matching the search query across all pages - * @param start zero-based offset indicating the starting position of returned results + * @param start zero-based offset indicating the starting position of returned results * @param maxScore highest relevance score among the returned documents (null if scoring disabled) * @param documents list of document maps containing field names and values for each result * @param facets nested map structure containing facet field names, values, and document counts - * * @version 0.0.1 * @since 0.0.1 - * * @see SearchService#search(String, String, List, List, List, Integer, Integer) * @see org.apache.solr.client.solrj.response.QueryResponse * @see org.apache.solr.common.SolrDocumentList @@ -109,6 +114,4 @@ public record SearchResponse( long start, Float maxScore, List> documents, - Map> facets -) { -} \ No newline at end of file + Map> facets) {} 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 16d02dc..babc472 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 @@ -16,6 +16,10 @@ */ package org.apache.solr.mcp.server.search; +import java.io.IOException; +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.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; @@ -29,53 +33,51 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** - * Spring Service providing comprehensive search capabilities for Apache Solr collections - * through Model Context Protocol (MCP) integration. - * - *

This service serves as the primary interface for executing search operations against - * Solr collections, offering a rich set of features including text search, filtering, - * faceting, sorting, and pagination. It transforms complex Solr query syntax into - * accessible MCP tools that AI clients can invoke through natural language requests.

- * - *

Core Features:

+ * Spring Service providing comprehensive search capabilities for Apache Solr collections through + * Model Context Protocol (MCP) integration. + * + *

This service serves as the primary interface for executing search operations against Solr + * collections, offering a rich set of features including text search, filtering, faceting, sorting, + * and pagination. It transforms complex Solr query syntax into accessible MCP tools that AI clients + * can invoke through natural language requests. + * + *

Core Features: + * *

    - *
  • Full-Text Search: Advanced text search with relevance scoring
  • - *
  • Filtering: Multi-criteria filtering using Solr filter queries
  • - *
  • Faceting: Dynamic facet generation for result categorization
  • - *
  • Sorting: Flexible result ordering by multiple fields
  • - *
  • Pagination: Efficient handling of large result sets
  • + *
  • Full-Text Search: Advanced text search with relevance scoring + *
  • Filtering: Multi-criteria filtering using Solr filter queries + *
  • Faceting: Dynamic facet generation for result categorization + *
  • Sorting: Flexible result ordering by multiple fields + *
  • Pagination: Efficient handling of large result sets *
- * - *

Dynamic Field Support:

- *

The service handles Solr's dynamic field naming conventions where field names - * include type suffixes that indicate data types and indexing behavior:

+ * + *

Dynamic Field Support: + * + *

The service handles Solr's dynamic field naming conventions where field names include type + * suffixes that indicate data types and indexing behavior: + * *

    - *
  • _s: String fields for exact matching
  • - *
  • _t: Text fields with tokenization and analysis
  • - *
  • _i, _l, _f, _d: Numeric fields (int, long, float, double)
  • - *
  • _dt: Date/time fields
  • - *
  • _b: Boolean fields
  • + *
  • _s: String fields for exact matching + *
  • _t: Text fields with tokenization and analysis + *
  • _i, _l, _f, _d: Numeric fields (int, long, float, double) + *
  • _dt: Date/time fields + *
  • _b: Boolean fields *
- * - *

MCP Tool Integration:

- *

Search operations are exposed as MCP tools that AI clients can invoke through - * natural language requests such as "search for books by George R.R. Martin" or - * "find products under $50 in the electronics category".

- * - *

Response Format:

- *

Returns structured {@link SearchResponse} objects that encapsulate search results, - * metadata, and facet information in a format optimized for JSON serialization and - * consumption by AI clients.

+ * + *

MCP Tool Integration: + * + *

Search operations are exposed as MCP tools that AI clients can invoke through natural language + * requests such as "search for books by George R.R. Martin" or "find products under $50 in the + * electronics category". + * + *

Response Format: + * + *

Returns structured {@link SearchResponse} objects that encapsulate search results, metadata, + * and facet information in a format optimized for JSON serialization and consumption by AI clients. * * @version 0.0.1 * @since 0.0.1 - * * @see SearchResponse * @see SolrClient * @see org.springframework.ai.tool.annotation.Tool @@ -89,13 +91,12 @@ public class SearchService { /** * Constructs a new SearchService with the required SolrClient dependency. - * - *

This constructor is automatically called by Spring's dependency injection - * framework during application startup, providing the service with the necessary - * Solr client for executing search operations.

+ * + *

This constructor is automatically called by Spring's dependency injection framework during + * application startup, providing the service with the necessary Solr client for executing + * search operations. * * @param solrClient the SolrJ client instance for communicating with Solr - * * @see SolrClient */ public SearchService(SolrClient solrClient) { @@ -104,38 +105,40 @@ public SearchService(SolrClient solrClient) { /** * Converts a SolrDocumentList to a List of Maps for optimized JSON serialization. - * - *

This method transforms Solr's native document format into a structure that - * can be easily serialized to JSON and consumed by MCP clients. Each document - * becomes a flat map of field names to field values, preserving all data types.

- * - *

Conversion Process:

+ * + *

This method transforms Solr's native document format into a structure that can be easily + * serialized to JSON and consumed by MCP clients. Each document becomes a flat map of field + * names to field values, preserving all data types. + * + *

Conversion Process: + * *

    - *
  • Iterates through each SolrDocument in the list
  • - *
  • Extracts all field names and their corresponding values
  • - *
  • Creates a HashMap for each document with field-value pairs
  • - *
  • Preserves original data types (strings, numbers, dates, arrays)
  • + *
  • Iterates through each SolrDocument in the list + *
  • Extracts all field names and their corresponding values + *
  • Creates a HashMap for each document with field-value pairs + *
  • Preserves original data types (strings, numbers, dates, arrays) *
- * - *

Performance Optimization:

- *

Pre-allocates the ArrayList with the known document count to minimize - * memory allocations and improve conversion performance for large result sets.

+ * + *

Performance Optimization: + * + *

Pre-allocates the ArrayList with the known document count to minimize memory allocations + * and improve conversion performance for large result sets. * * @param documents the SolrDocumentList to convert from Solr's native format * @return a List of Maps where each Map represents a document with field names as keys - * * @see org.apache.solr.common.SolrDocument * @see org.apache.solr.common.SolrDocumentList */ private static List> getDocs(SolrDocumentList documents) { List> docs = new java.util.ArrayList<>(documents.size()); - documents.forEach(doc -> { - Map docMap = new HashMap<>(); - for (String fieldName : doc.getFieldNames()) { - docMap.put(fieldName, doc.getFieldValue(fieldName)); - } - docs.add(docMap); - }); + documents.forEach( + doc -> { + Map docMap = new HashMap<>(); + for (String fieldName : doc.getFieldNames()) { + docMap.put(fieldName, doc.getFieldValue(fieldName)); + } + docs.add(docMap); + }); return docs; } @@ -148,67 +151,78 @@ private static List> getDocs(SolrDocumentList documents) { private static Map> getFacets(QueryResponse queryResponse) { Map> facets = new HashMap<>(); if (queryResponse.getFacetFields() != null && !queryResponse.getFacetFields().isEmpty()) { - queryResponse.getFacetFields().forEach(facetField -> { - Map facetValues = new HashMap<>(); - for (FacetField.Count count : facetField.getValues()) { - facetValues.put(count.getName(), count.getCount()); - } - facets.put(facetField.getName(), facetValues); - }); + queryResponse + .getFacetFields() + .forEach( + facetField -> { + Map facetValues = new HashMap<>(); + for (FacetField.Count count : facetField.getValues()) { + facetValues.put(count.getName(), count.getCount()); + } + facets.put(facetField.getName(), facetValues); + }); } return facets; } - /** - * Searches a Solr collection with the specified parameters. - * This method is exposed as a tool for MCP clients to use. + * Searches a Solr collection with the specified parameters. This method is exposed as a tool + * for MCP clients to use. * - * @param collection The Solr collection to query - * @param query The Solr query string (q parameter). Defaults to "*:*" if not specified + * @param collection The Solr collection to query + * @param query The Solr query string (q parameter). Defaults to "*:*" if not specified * @param filterQueries List of filter queries (fq parameter) - * @param facetFields List of fields to facet on - * @param sortClauses List of sort clauses for ordering results - * @param start Starting offset for pagination - * @param rows Number of rows to return + * @param facetFields List of fields to facet on + * @param sortClauses List of sort clauses for ordering results + * @param start Starting offset for pagination + * @param rows Number of rows to return * @return A SearchResponse containing the search results and facets * @throws SolrServerException If there's an error communicating with Solr - * @throws IOException If there's an I/O error + * @throws IOException If there's an I/O error */ - @McpTool(name = "Search", - description = """ - Search specified Solr collection with query, optional filters, facets, sorting, and pagination. - Note that solr has dynamic fields where name of field in schema may end with suffixes - _s: Represents a string field, used for exact string matching. - _i: Represents an integer field. - _l: Represents a long field. - _f: Represents a float field. - _d: Represents a double field. - _dt: Represents a date field. - _b: Represents a boolean field. - _t: Often used for text fields that undergo tokenization and analysis. - One example from the books collection: - { - "id":"0553579908", - "cat":["book"], - "name":["A Clash of Kings"], - "price":[7.99], - "inStock":[true], - "author":["George R.R. Martin"], - "series_t":"A Song of Ice and Fire", - "sequence_i":2, - "genre_s":"fantasy", - "_version_":1836275819373133824, - "_root_":"0553579908" - } - """) + @McpTool( + name = "Search", + description = + """ +Search specified Solr collection with query, optional filters, facets, sorting, and pagination. +Note that solr has dynamic fields where name of field in schema may end with suffixes +_s: Represents a string field, used for exact string matching. +_i: Represents an integer field. +_l: Represents a long field. +_f: Represents a float field. +_d: Represents a double field. +_dt: Represents a date field. +_b: Represents a boolean field. +_t: Often used for text fields that undergo tokenization and analysis. +One example from the books collection: +{ + "id":"0553579908", + "cat":["book"], + "name":["A Clash of Kings"], + "price":[7.99], + "inStock":[true], + "author":["George R.R. Martin"], + "series_t":"A Song of Ice and Fire", + "sequence_i":2, + "genre_s":"fantasy", + "_version_":1836275819373133824, + "_root_":"0553579908" + } +""") public SearchResponse search( @McpToolParam(description = "Solr collection to query") String collection, - @McpToolParam(description = "Solr q parameter. If none specified defaults to \"*:*\"", required = false) String query, - @McpToolParam(description = "Solr fq parameter", required = false) List filterQueries, - @McpToolParam(description = "Solr facet fields", required = false) List facetFields, - @McpToolParam(description = "Solr sort parameter", required = false) List> sortClauses, - @McpToolParam(description = "Starting offset for pagination", required = false) Integer start, + @McpToolParam( + description = "Solr q parameter. If none specified defaults to \"*:*\"", + required = false) + String query, + @McpToolParam(description = "Solr fq parameter", required = false) + List filterQueries, + @McpToolParam(description = "Solr facet fields", required = false) + List facetFields, + @McpToolParam(description = "Solr sort parameter", required = false) + List> sortClauses, + @McpToolParam(description = "Starting offset for pagination", required = false) + Integer start, @McpToolParam(description = "Number of rows to return", required = false) Integer rows) throws SolrServerException, IOException { @@ -233,10 +247,14 @@ public SearchResponse search( // sorting if (!CollectionUtils.isEmpty(sortClauses)) { - solrQuery.setSorts(sortClauses.stream() - .map(sortClause -> new SolrQuery.SortClause(sortClause.get(SORT_ITEM), - sortClause.get(SORT_ORDER))) - .toList()); + solrQuery.setSorts( + sortClauses.stream() + .map( + sortClause -> + new SolrQuery.SortClause( + sortClause.get(SORT_ITEM), + sortClause.get(SORT_ORDER))) + .toList()); } // pagination @@ -264,9 +282,6 @@ public SearchResponse search( documents.getStart(), documents.getMaxScore(), docs, - facets - ); - + facets); } - } 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 918e4d6..c0f434a 100644 --- a/src/test/java/org/apache/solr/mcp/server/ClientHttp.java +++ b/src/test/java/org/apache/solr/mcp/server/ClientHttp.java @@ -26,5 +26,4 @@ public static void main(String[] args) { var transport = HttpClientStreamableHttpTransport.builder("http://localhost:8080").build(); new SampleClient(transport).run(); } - -} \ No newline at end of file +} 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 00c0235..60ab695 100644 --- a/src/test/java/org/apache/solr/mcp/server/ClientStdio.java +++ b/src/test/java/org/apache/solr/mcp/server/ClientStdio.java @@ -20,24 +20,24 @@ import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.StdioClientTransport; import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; - import java.io.File; -// run after project has been built with "./gradlew build -x test and the mcp server jar is connected to a running solr" +// run after project has been built with "./gradlew build -x test and the mcp server jar is +// connected to a running solr" public class ClientStdio { public static void main(String[] args) { System.out.println(new File(".").getAbsolutePath()); - var stdioParams = ServerParameters.builder("java") - .args("-jar", - "build/libs/solr-mcp-server-0.0.1-SNAPSHOT.jar") - .build(); + var stdioParams = + ServerParameters.builder("java") + .args("-jar", "build/libs/solr-mcp-server-0.0.1-SNAPSHOT.jar") + .build(); - var transport = new StdioClientTransport(stdioParams, new JacksonMcpJsonMapper(new ObjectMapper())); + var transport = + new StdioClientTransport(stdioParams, new JacksonMcpJsonMapper(new ObjectMapper())); new SampleClient(transport).run(); } - -} \ No newline at end of file +} 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 c49a10b..4b5765b 100644 --- a/src/test/java/org/apache/solr/mcp/server/MainTest.java +++ b/src/test/java/org/apache/solr/mcp/server/MainTest.java @@ -26,29 +26,24 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; /** - * Application context loading test with mocked services. - * This test verifies that the Spring application context can be loaded successfully - * without requiring actual Solr connections, using mocked beans to prevent external dependencies. + * Application context loading test with mocked services. This test verifies that the Spring + * application context can be loaded successfully without requiring actual Solr connections, using + * mocked beans to prevent external dependencies. */ @SpringBootTest @ActiveProfiles("test") class MainTest { - @MockitoBean - private SearchService searchService; + @MockitoBean private SearchService searchService; - @MockitoBean - private IndexingService indexingService; + @MockitoBean private IndexingService indexingService; - @MockitoBean - private CollectionService collectionService; + @MockitoBean private CollectionService collectionService; - @MockitoBean - private SchemaService schemaService; + @MockitoBean private SchemaService schemaService; @Test void contextLoads() { // Context loading test - all services are mocked to prevent Solr API calls } - } diff --git a/src/test/java/org/apache/solr/mcp/server/McpToolRegistrationTest.java b/src/test/java/org/apache/solr/mcp/server/McpToolRegistrationTest.java index 08239f9..15da485 100644 --- a/src/test/java/org/apache/solr/mcp/server/McpToolRegistrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/McpToolRegistrationTest.java @@ -16,6 +16,12 @@ */ package org.apache.solr.mcp.server; +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; import org.apache.solr.mcp.server.indexing.IndexingService; import org.apache.solr.mcp.server.metadata.CollectionService; import org.apache.solr.mcp.server.metadata.SchemaService; @@ -24,70 +30,68 @@ import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpToolParam; -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.Arrays; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - /** - * Tests for MCP tool registration and annotation validation. - * Ensures all services expose their methods correctly as MCP tools - * with proper annotations and descriptions. + * Tests for MCP tool registration and annotation validation. Ensures all services expose their + * methods correctly as MCP tools with proper annotations and descriptions. */ class McpToolRegistrationTest { @Test void testSearchServiceHasToolAnnotation() throws NoSuchMethodException { // Get the search method from SearchService - Method searchMethod = SearchService.class.getMethod("search", - String.class, - String.class, - List.class, - List.class, - List.class, - Integer.class, - Integer.class); + Method searchMethod = + SearchService.class.getMethod( + "search", + String.class, + String.class, + List.class, + List.class, + List.class, + Integer.class, + Integer.class); // Verify it has the @McpTool annotation - assertTrue(searchMethod.isAnnotationPresent(McpTool.class), + assertTrue( + searchMethod.isAnnotationPresent(McpTool.class), "SearchService.search method should have @McpTool annotation"); // Verify the annotation properties McpTool toolAnnotation = searchMethod.getAnnotation(McpTool.class); - assertEquals("Search", toolAnnotation.name(), - "McpTool name should be 'Search'"); - assertNotNull(toolAnnotation.description(), - "McpTool description should not be null"); - assertFalse(toolAnnotation.description().isBlank(), - "McpTool description should not be blank"); + assertEquals("Search", toolAnnotation.name(), "McpTool name should be 'Search'"); + assertNotNull(toolAnnotation.description(), "McpTool description should not be null"); + assertFalse( + toolAnnotation.description().isBlank(), "McpTool description should not be blank"); } @Test void testSearchServiceToolParametersHaveAnnotations() throws NoSuchMethodException { // Get the search method - Method searchMethod = SearchService.class.getMethod("search", - String.class, - String.class, - List.class, - List.class, - List.class, - Integer.class, - Integer.class); + Method searchMethod = + SearchService.class.getMethod( + "search", + String.class, + String.class, + List.class, + List.class, + List.class, + Integer.class, + Integer.class); // Verify all parameters have @McpToolParam annotations Parameter[] parameters = searchMethod.getParameters(); assertTrue(parameters.length > 0, "Search method should have parameters"); for (Parameter param : parameters) { - assertTrue(param.isAnnotationPresent(McpToolParam.class), + assertTrue( + param.isAnnotationPresent(McpToolParam.class), "Parameter " + param.getName() + " should have @McpToolParam annotation"); McpToolParam paramAnnotation = param.getAnnotation(McpToolParam.class); - assertNotNull(paramAnnotation.description(), + assertNotNull( + paramAnnotation.description(), "Parameter " + param.getName() + " should have description"); - assertFalse(paramAnnotation.description().isBlank(), + assertFalse( + paramAnnotation.description().isBlank(), "Parameter " + param.getName() + " description should not be blank"); } } @@ -98,12 +102,12 @@ void testIndexingServiceHasToolAnnotations() { Method[] methods = IndexingService.class.getDeclaredMethods(); // Find methods with @McpTool annotation - List mcpToolMethods = Arrays.stream(methods) - .filter(m -> m.isAnnotationPresent(McpTool.class)) - .toList(); + List mcpToolMethods = + Arrays.stream(methods).filter(m -> m.isAnnotationPresent(McpTool.class)).toList(); // Verify at least one method has the annotation - assertFalse(mcpToolMethods.isEmpty(), + assertFalse( + mcpToolMethods.isEmpty(), "IndexingService should have at least one method with @McpTool annotation"); // Verify each tool has proper annotations @@ -112,7 +116,8 @@ void testIndexingServiceHasToolAnnotations() { assertNotNull(toolAnnotation.name(), "Tool name should not be null"); assertFalse(toolAnnotation.name().isBlank(), "Tool name should not be blank"); assertNotNull(toolAnnotation.description(), "Tool description should not be null"); - assertFalse(toolAnnotation.description().isBlank(), "Tool description should not be blank"); + assertFalse( + toolAnnotation.description().isBlank(), "Tool description should not be blank"); } } @@ -122,19 +127,20 @@ void testCollectionServiceHasToolAnnotations() { Method[] methods = CollectionService.class.getDeclaredMethods(); // Find methods with @McpTool annotation - List mcpToolMethods = Arrays.stream(methods) - .filter(m -> m.isAnnotationPresent(McpTool.class)) - .toList(); + List mcpToolMethods = + Arrays.stream(methods).filter(m -> m.isAnnotationPresent(McpTool.class)).toList(); // Verify at least one method has the annotation - assertFalse(mcpToolMethods.isEmpty(), + assertFalse( + mcpToolMethods.isEmpty(), "CollectionService should have at least one method with @McpTool annotation"); // Verify each tool has proper annotations for (Method method : mcpToolMethods) { McpTool toolAnnotation = method.getAnnotation(McpTool.class); assertNotNull(toolAnnotation.description(), "Tool description should not be null"); - assertFalse(toolAnnotation.description().isBlank(), "Tool description should not be blank"); + assertFalse( + toolAnnotation.description().isBlank(), "Tool description should not be blank"); } } @@ -144,19 +150,20 @@ void testSchemaServiceHasToolAnnotations() { Method[] methods = SchemaService.class.getDeclaredMethods(); // Find methods with @McpTool annotation - List mcpToolMethods = Arrays.stream(methods) - .filter(m -> m.isAnnotationPresent(McpTool.class)) - .toList(); + List mcpToolMethods = + Arrays.stream(methods).filter(m -> m.isAnnotationPresent(McpTool.class)).toList(); // Verify at least one method has the annotation - assertFalse(mcpToolMethods.isEmpty(), + assertFalse( + mcpToolMethods.isEmpty(), "SchemaService should have at least one method with @McpTool annotation"); // Verify each tool has proper annotations for (Method method : mcpToolMethods) { McpTool toolAnnotation = method.getAnnotation(McpTool.class); assertNotNull(toolAnnotation.description(), "Tool description should not be null"); - assertFalse(toolAnnotation.description().isBlank(), "Tool description should not be blank"); + assertFalse( + toolAnnotation.description().isBlank(), "Tool description should not be blank"); } } @@ -179,34 +186,41 @@ void testAllMcpToolsHaveUniqueNames() { // Verify all tool names are unique long uniqueCount = toolNames.stream().distinct().count(); - assertEquals(toolNames.size(), uniqueCount, - "All MCP tool names should be unique across all services. Found tools: " + toolNames); + assertEquals( + toolNames.size(), + uniqueCount, + "All MCP tool names should be unique across all services. Found tools: " + + toolNames); } @Test void testMcpToolParametersFollowConventions() throws NoSuchMethodException { // Get the search method - Method searchMethod = SearchService.class.getMethod("search", - String.class, - String.class, - List.class, - List.class, - List.class, - Integer.class, - Integer.class); + Method searchMethod = + SearchService.class.getMethod( + "search", + String.class, + String.class, + List.class, + List.class, + List.class, + Integer.class, + Integer.class); Parameter[] parameters = searchMethod.getParameters(); // Verify first parameter (collection) is required McpToolParam firstParam = parameters[0].getAnnotation(McpToolParam.class); - assertTrue(firstParam.required() || !firstParam.required(), + assertTrue( + firstParam.required() || !firstParam.required(), "First parameter annotation should specify required status"); // Verify optional parameters have required=false for (int i = 1; i < parameters.length; i++) { McpToolParam param = parameters[i].getAnnotation(McpToolParam.class); // Optional parameters should be marked as such in description or required flag - assertNotNull(param.description(), + assertNotNull( + param.description(), "Parameter should have description indicating if it's optional"); } } @@ -216,12 +230,13 @@ private void addToolNames(Class serviceClass, List toolNames) { Method[] methods = serviceClass.getDeclaredMethods(); Arrays.stream(methods) .filter(m -> m.isAnnotationPresent(McpTool.class)) - .forEach(m -> { - McpTool annotation = m.getAnnotation(McpTool.class); - // Use name if provided, otherwise use method name - String toolName = annotation.name().isBlank() ? m.getName() : annotation.name(); - toolNames.add(toolName); - }); + .forEach( + m -> { + McpTool annotation = m.getAnnotation(McpTool.class); + // Use name if provided, otherwise use method name + String toolName = + annotation.name().isBlank() ? m.getName() : annotation.name(); + toolNames.add(toolName); + }); } } - diff --git a/src/test/java/org/apache/solr/mcp/server/SampleClient.java b/src/test/java/org/apache/solr/mcp/server/SampleClient.java index 40045a3..6ceae3b 100644 --- a/src/test/java/org/apache/solr/mcp/server/SampleClient.java +++ b/src/test/java/org/apache/solr/mcp/server/SampleClient.java @@ -16,64 +16,65 @@ */ package org.apache.solr.mcp.server; +import static org.junit.jupiter.api.Assertions.*; + import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; import io.modelcontextprotocol.spec.McpSchema.Tool; - import java.util.List; import java.util.Set; -import static org.junit.jupiter.api.Assertions.*; - /** * Sample MCP client for testing and demonstrating Solr MCP Server functionality. * - *

This test client provides a comprehensive validation suite for the Solr MCP Server, - * verifying that all expected MCP tools are properly registered and functioning as expected. - * It serves as both a testing framework and a reference implementation for MCP client integration.

+ *

This test client provides a comprehensive validation suite for the Solr MCP Server, verifying + * that all expected MCP tools are properly registered and functioning as expected. It serves as + * both a testing framework and a reference implementation for MCP client integration. + * + *

Test Coverage: * - *

Test Coverage:

*
    - *
  • Client Initialization: Verifies MCP client can connect and initialize
  • - *
  • Connection Health: Tests ping functionality and connection stability
  • - *
  • Tool Discovery: Validates all expected MCP tools are registered
  • - *
  • Tool Validation: Checks tool metadata, descriptions, and schemas
  • - *
  • Expected Tools: Verifies presence of search, indexing, and metadata tools
  • + *
  • Client Initialization: Verifies MCP client can connect and initialize + *
  • Connection Health: Tests ping functionality and connection stability + *
  • Tool Discovery: Validates all expected MCP tools are registered + *
  • Tool Validation: Checks tool metadata, descriptions, and schemas + *
  • Expected Tools: Verifies presence of search, indexing, and metadata tools *
* - *

Expected MCP Tools:

+ *

Expected MCP Tools: + * *

    - *
  • index_json_documents: JSON document indexing capability
  • - *
  • index_csv_documents: CSV document indexing capability
  • - *
  • index_xml_documents: XML document indexing capability
  • - *
  • Search: Full-text search functionality with filtering and faceting
  • - *
  • listCollections: Collection discovery and listing
  • - *
  • getCollectionStats: Collection metrics and performance data
  • - *
  • checkHealth: Health monitoring and status reporting
  • - *
  • getSchema: Schema introspection and field analysis
  • + *
  • index_json_documents: JSON document indexing capability + *
  • index_csv_documents: CSV document indexing capability + *
  • index_xml_documents: XML document indexing capability + *
  • Search: Full-text search functionality with filtering and faceting + *
  • listCollections: Collection discovery and listing + *
  • getCollectionStats: Collection metrics and performance data + *
  • checkHealth: Health monitoring and status reporting + *
  • getSchema: Schema introspection and field analysis *
* - *

Usage Example:

+ *

Usage Example: + * *

{@code
  * McpClientTransport transport = // ... initialize transport
  * SampleClient client = new SampleClient(transport);
  * client.run(); // Executes full test suite
  * }
* - *

Assertion Strategy:

- *

Uses JUnit assertions to validate expected behavior and fail fast on any - * inconsistencies. Each tool is validated for proper name, description, and schema - * configuration to ensure MCP protocol compliance.

+ *

Assertion Strategy: + * + *

Uses JUnit assertions to validate expected behavior and fail fast on any inconsistencies. Each + * tool is validated for proper name, description, and schema configuration to ensure MCP protocol + * compliance. * * @version 0.0.1 * @since 0.0.1 - * * @see McpClient * @see McpClientTransport * @see io.modelcontextprotocol.spec.McpSchema.Tool */ - public class SampleClient { private final McpClientTransport transport; @@ -91,36 +92,41 @@ public SampleClient(McpClientTransport transport) { /** * Executes the comprehensive test suite for Solr MCP Server functionality. * - *

This method performs a complete validation of the MCP server including:

+ *

This method performs a complete validation of the MCP server including: + * *

    - *
  • Client initialization and connection establishment
  • - *
  • Health check via ping operation
  • - *
  • Tool discovery and count validation
  • - *
  • Individual tool metadata validation
  • - *
  • Tool-specific description and schema verification
  • + *
  • Client initialization and connection establishment + *
  • Health check via ping operation + *
  • Tool discovery and count validation + *
  • Individual tool metadata validation + *
  • Tool-specific description and schema verification *
* - *

Test Sequence:

+ *

Test Sequence: + * *

    - *
  1. Initialize MCP client with provided transport
  2. - *
  3. Perform ping test to verify connectivity
  4. - *
  5. List all available tools and validate expected count (8 tools)
  6. - *
  7. Verify each expected tool is present in the tools list
  8. - *
  9. Validate tool metadata (name, description, schema) for each tool
  10. - *
  11. Perform tool-specific validation based on tool type
  12. + *
  13. Initialize MCP client with provided transport + *
  14. Perform ping test to verify connectivity + *
  15. List all available tools and validate expected count (8 tools) + *
  16. Verify each expected tool is present in the tools list + *
  17. Validate tool metadata (name, description, schema) for each tool + *
  18. Perform tool-specific validation based on tool type *
* * @throws RuntimeException if any test assertion fails or MCP operations encounter errors - * @throws AssertionError if expected tools are missing or tool validation fails + * @throws AssertionError if expected tools are missing or tool validation fails */ public void run() { - try (var client = McpClient.sync(this.transport) - .loggingConsumer(message -> System.out.println(">> Client Logging: " + message)) - .build()) { + try (var client = + McpClient.sync(this.transport) + .loggingConsumer( + message -> System.out.println(">> Client Logging: " + message)) + .build()) { // Assert client initialization succeeds - assertDoesNotThrow(client::initialize, "Client initialization should not throw an exception"); + assertDoesNotThrow( + client::initialize, "Client initialization should not throw an exception"); // Assert ping succeeds assertDoesNotThrow(client::ping, "Client ping should not throw an exception"); @@ -134,69 +140,92 @@ public void run() { assertEquals(8, toolsList.tools().size(), "Expected 8 tools to be available"); // Define expected tools based on the log output - Set expectedToolNames = Set.of( - "index_json_documents", - "index_csv_documents", - "getCollectionStats", - "Search", - "listCollections", - "checkHealth", - "index_xml_documents", - "getSchema" - ); + Set expectedToolNames = + Set.of( + "index_json_documents", + "index_csv_documents", + "getCollectionStats", + "Search", + "listCollections", + "checkHealth", + "index_xml_documents", + "getSchema"); // Validate each expected tool is present - List actualToolNames = toolsList.tools().stream() - .map(Tool::name) - .toList(); + List actualToolNames = toolsList.tools().stream().map(Tool::name).toList(); for (String expectedTool : expectedToolNames) { - assertTrue(actualToolNames.contains(expectedTool), + assertTrue( + actualToolNames.contains(expectedTool), "Expected tool '" + expectedTool + "' should be available"); } // Validate tool details for key tools - toolsList.tools().forEach(tool -> { - assertNotNull(tool.name(), "Tool name should not be null"); - assertNotNull(tool.description(), "Tool description should not be null"); - assertNotNull(tool.inputSchema(), "Tool input schema should not be null"); - assertFalse(tool.name().trim().isEmpty(), "Tool name should not be empty"); - assertFalse(tool.description().trim().isEmpty(), "Tool description should not be empty"); - - // Validate specific tools based on expected behavior - switch (tool.name()) { - case "index_json_documents": - assertTrue(tool.description().toLowerCase().contains("json"), - "JSON indexing tool should mention JSON in description"); - break; - case "index_csv_documents": - assertTrue(tool.description().toLowerCase().contains("csv"), - "CSV indexing tool should mention CSV in description"); - break; - case "Search": - assertTrue(tool.description().toLowerCase().contains("search"), - "Search tool should mention search in description"); - break; - case "listCollections": - assertTrue(tool.description().toLowerCase().contains("collection"), - "List collections tool should mention collections in description"); - break; - case "checkHealth": - assertTrue(tool.description().toLowerCase().contains("health"), - "Health check tool should mention health in description"); - break; - default: - // Additional tools are acceptable - break; - } - - System.out.println("Tool: " + tool.name() + ", description: " + tool.description() + ", schema: " - + tool.inputSchema()); - }); + toolsList + .tools() + .forEach( + tool -> { + assertNotNull(tool.name(), "Tool name should not be null"); + assertNotNull( + tool.description(), "Tool description should not be null"); + assertNotNull( + tool.inputSchema(), "Tool input schema should not be null"); + assertFalse( + tool.name().trim().isEmpty(), + "Tool name should not be empty"); + assertFalse( + tool.description().trim().isEmpty(), + "Tool description should not be empty"); + + // Validate specific tools based on expected behavior + switch (tool.name()) { + case "index_json_documents": + assertTrue( + tool.description().toLowerCase().contains("json"), + "JSON indexing tool should mention JSON in" + + " description"); + break; + case "index_csv_documents": + assertTrue( + tool.description().toLowerCase().contains("csv"), + "CSV indexing tool should mention CSV in" + + " description"); + break; + case "Search": + assertTrue( + tool.description().toLowerCase().contains("search"), + "Search tool should mention search in description"); + break; + case "listCollections": + assertTrue( + tool.description() + .toLowerCase() + .contains("collection"), + "List collections tool should mention collections" + + " in description"); + break; + case "checkHealth": + assertTrue( + tool.description().toLowerCase().contains("health"), + "Health check tool should mention health in" + + " description"); + break; + default: + // Additional tools are acceptable + break; + } + + System.out.println( + "Tool: " + + tool.name() + + ", description: " + + tool.description() + + ", schema: " + + tool.inputSchema()); + }); } catch (Exception e) { throw new RuntimeException("MCP client operation failed", e); } } - -} \ No newline at end of file +} diff --git a/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java b/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java index 046d16f..ea73984 100644 --- a/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java +++ b/src/test/java/org/apache/solr/mcp/server/TestcontainersConfiguration.java @@ -34,6 +34,14 @@ SolrContainer solr() { @Bean DynamicPropertyRegistrar propertiesRegistrar(SolrContainer solr) { - return registry -> registry.add("solr.url", () -> "http://" + solr.getHost() + ":" + solr.getMappedPort(SOLR_PORT) + "/solr/"); + return registry -> + registry.add( + "solr.url", + () -> + "http://" + + solr.getHost() + + ":" + + solr.getMappedPort(SOLR_PORT) + + "/solr/"); } } 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 e3e6081..12da2b8 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 @@ -16,6 +16,8 @@ */ package org.apache.solr.mcp.server.config; +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.mcp.server.TestcontainersConfiguration; @@ -27,20 +29,15 @@ import org.springframework.context.annotation.Import; import org.testcontainers.containers.SolrContainer; -import static org.junit.jupiter.api.Assertions.*; - @SpringBootTest @Import(TestcontainersConfiguration.class) class SolrConfigTest { - @Autowired - private SolrClient solrClient; + @Autowired private SolrClient solrClient; - @Autowired - SolrContainer solrContainer; + @Autowired SolrContainer solrContainer; - @Autowired - private SolrConfigurationProperties properties; + @Autowired private SolrConfigurationProperties properties; @Test void testSolrClientConfiguration() { @@ -48,9 +45,15 @@ void testSolrClientConfiguration() { assertNotNull(solrClient); // Verify that the SolrClient is using the correct URL - // Note: SolrConfig normalizes the URL to have trailing slash, but Http2SolrClient removes it + // Note: SolrConfig normalizes the URL to have trailing slash, but Http2SolrClient removes + // it var httpSolrClient = assertInstanceOf(Http2SolrClient.class, solrClient); - String expectedUrl = "http://" + solrContainer.getHost() + ":" + solrContainer.getMappedPort(8983) + "/solr"; + String expectedUrl = + "http://" + + solrContainer.getHost() + + ":" + + solrContainer.getMappedPort(8983) + + "/solr"; assertEquals(expectedUrl, httpSolrClient.getBaseURL()); } @@ -59,7 +62,12 @@ void testSolrConfigurationProperties() { // Verify that the properties are correctly loaded assertNotNull(properties); assertNotNull(properties.url()); - assertEquals("http://" + solrContainer.getHost() + ":" + solrContainer.getMappedPort(8983) + "/solr/", + assertEquals( + "http://" + + solrContainer.getHost() + + ":" + + solrContainer.getMappedPort(8983) + + "/solr/", properties.url()); } @@ -74,17 +82,17 @@ void testSolrConfigurationProperties() { 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); assertNotNull(client); - + var httpClient = assertInstanceOf(Http2SolrClient.class, client); assertEquals(expectedUrl, httpClient.getBaseURL()); - + // Clean up try { client.close(); @@ -96,15 +104,16 @@ void testUrlNormalization(String inputUrl, String expectedUrl) { @Test void testUrlWithoutTrailingSlash() { // Test URL without trailing slash branch - SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983"); + SolrConfigurationProperties testProperties = + new SolrConfigurationProperties("http://localhost:8983"); SolrConfig solrConfig = new SolrConfig(); - + SolrClient client = solrConfig.solrClient(testProperties); Http2SolrClient httpClient = (Http2SolrClient) client; - + // Should add trailing slash and solr path assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); - + try { client.close(); } catch (Exception e) { @@ -115,15 +124,16 @@ void testUrlWithoutTrailingSlash() { @Test void testUrlWithTrailingSlashButNoSolrPath() { // Test URL with trailing slash but no solr path branch - SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/"); + SolrConfigurationProperties testProperties = + new SolrConfigurationProperties("http://localhost:8983/"); SolrConfig solrConfig = new SolrConfig(); - + SolrClient client = solrConfig.solrClient(testProperties); 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) { @@ -134,15 +144,16 @@ void testUrlWithTrailingSlashButNoSolrPath() { @Test void testUrlWithSolrPathButNoTrailingSlash() { // Test URL with solr path but no trailing slash - SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/solr"); + SolrConfigurationProperties testProperties = + new SolrConfigurationProperties("http://localhost:8983/solr"); SolrConfig solrConfig = new SolrConfig(); - + SolrClient client = solrConfig.solrClient(testProperties); Http2SolrClient httpClient = (Http2SolrClient) client; - + // Should add trailing slash assertEquals("http://localhost:8983/solr", httpClient.getBaseURL()); - + try { client.close(); } catch (Exception e) { @@ -153,19 +164,20 @@ void testUrlWithSolrPathButNoTrailingSlash() { @Test void testUrlAlreadyProperlyFormatted() { // Test URL that's already properly formatted - SolrConfigurationProperties testProperties = new SolrConfigurationProperties("http://localhost:8983/solr/"); + SolrConfigurationProperties testProperties = + new SolrConfigurationProperties("http://localhost:8983/solr/"); SolrConfig solrConfig = new SolrConfig(); - + SolrClient client = solrConfig.solrClient(testProperties); 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 } } -} \ No newline at end of file +} diff --git a/src/test/java/org/apache/solr/mcp/server/indexing/CsvIndexingTest.java b/src/test/java/org/apache/solr/mcp/server/indexing/CsvIndexingTest.java index aaa541c..b22c83c 100644 --- a/src/test/java/org/apache/solr/mcp/server/indexing/CsvIndexingTest.java +++ b/src/test/java/org/apache/solr/mcp/server/indexing/CsvIndexingTest.java @@ -16,6 +16,9 @@ */ package org.apache.solr.mcp.server.indexing; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.mcp.server.indexing.documentcreator.IndexingDocumentCreator; import org.junit.jupiter.api.Test; @@ -23,40 +26,37 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - /** * Test class for CSV indexing functionality in IndexingService. - * - *

This test verifies that the IndexingService can correctly parse CSV data - * and convert it into SolrInputDocument objects using the schema-less approach.

+ * + *

This test verifies that the IndexingService can correctly parse CSV data and convert it into + * SolrInputDocument objects using the schema-less approach. */ @SpringBootTest @TestPropertySource(locations = "classpath:application.properties") class CsvIndexingTest { - @Autowired - private IndexingDocumentCreator indexingDocumentCreator; + @Autowired private IndexingDocumentCreator indexingDocumentCreator; @Test void testCreateSchemalessDocumentsFromCsv() throws Exception { // Given - - String csvData = """ - id,cat,name,price,inStock,author,series_t,sequence_i,genre_s - 0553573403,book,A Game of Thrones,7.99,true,George R.R. Martin,"A Song of Ice and Fire",1,fantasy - 0553579908,book,A Clash of Kings,7.99,true,George R.R. Martin,"A Song of Ice and Fire",2,fantasy - 0553293354,book,Foundation,7.99,true,Isaac Asimov,Foundation Novels,1,scifi - """; - + + String csvData = + """ +id,cat,name,price,inStock,author,series_t,sequence_i,genre_s +0553573403,book,A Game of Thrones,7.99,true,George R.R. Martin,"A Song of Ice and Fire",1,fantasy +0553579908,book,A Clash of Kings,7.99,true,George R.R. Martin,"A Song of Ice and Fire",2,fantasy +0553293354,book,Foundation,7.99,true,Isaac Asimov,Foundation Novels,1,scifi +"""; + // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromCsv(csvData); - + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromCsv(csvData); + // Then assertThat(documents).hasSize(3); - + // Verify first document SolrInputDocument firstDoc = documents.getFirst(); assertThat(firstDoc.getFieldValue("id")).isEqualTo("0553573403"); @@ -68,13 +68,13 @@ void testCreateSchemalessDocumentsFromCsv() throws Exception { assertThat(firstDoc.getFieldValue("series_t")).isEqualTo("A Song of Ice and Fire"); assertThat(firstDoc.getFieldValue("sequence_i")).isEqualTo("1"); assertThat(firstDoc.getFieldValue("genre_s")).isEqualTo("fantasy"); - + // Verify second document SolrInputDocument secondDoc = documents.get(1); assertThat(secondDoc.getFieldValue("id")).isEqualTo("0553579908"); assertThat(secondDoc.getFieldValue("name")).isEqualTo("A Clash of Kings"); assertThat(secondDoc.getFieldValue("sequence_i")).isEqualTo("2"); - + // Verify third document SolrInputDocument thirdDoc = documents.get(2); assertThat(thirdDoc.getFieldValue("id")).isEqualTo("0553293354"); @@ -82,69 +82,73 @@ void testCreateSchemalessDocumentsFromCsv() throws Exception { assertThat(thirdDoc.getFieldValue("author")).isEqualTo("Isaac Asimov"); assertThat(thirdDoc.getFieldValue("genre_s")).isEqualTo("scifi"); } - + @Test void testCreateSchemalessDocumentsFromCsvWithEmptyValues() throws Exception { // Given - - String csvData = """ - id,name,description - 1,Test Product,Some description - 2,Another Product, - 3,,Empty name - """; - + + String csvData = + """ + id,name,description + 1,Test Product,Some description + 2,Another Product, + 3,,Empty name + """; + // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromCsv(csvData); - + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromCsv(csvData); + // Then assertThat(documents).hasSize(3); - + // First document should have all fields SolrInputDocument firstDoc = documents.getFirst(); assertThat(firstDoc.getFieldValue("id")).isEqualTo("1"); assertThat(firstDoc.getFieldValue("name")).isEqualTo("Test Product"); assertThat(firstDoc.getFieldValue("description")).isEqualTo("Some description"); - + // Second document should skip empty description SolrInputDocument secondDoc = documents.get(1); assertThat(secondDoc.getFieldValue("id")).isEqualTo("2"); assertThat(secondDoc.getFieldValue("name")).isEqualTo("Another Product"); assertThat(secondDoc.getFieldValue("description")).isNull(); - + // Third document should skip empty name SolrInputDocument thirdDoc = documents.get(2); assertThat(thirdDoc.getFieldValue("id")).isEqualTo("3"); assertThat(thirdDoc.getFieldValue("name")).isNull(); assertThat(thirdDoc.getFieldValue("description")).isEqualTo("Empty name"); } - + @Test void testCreateSchemalessDocumentsFromCsvWithQuotedValues() throws Exception { // Given - - String csvData = """ - id,name,description - 1,"Quoted Name","Quoted description" - 2,Regular Name,Regular description - """; - + + String csvData = + """ + id,name,description + 1,"Quoted Name","Quoted description" + 2,Regular Name,Regular description + """; + // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromCsv(csvData); - + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromCsv(csvData); + // Then assertThat(documents).hasSize(2); - + // First document should have quotes removed SolrInputDocument firstDoc = documents.getFirst(); assertThat(firstDoc.getFieldValue("id")).isEqualTo("1"); assertThat(firstDoc.getFieldValue("name")).isEqualTo("Quoted Name"); assertThat(firstDoc.getFieldValue("description")).isEqualTo("Quoted description"); - + // Second document should remain unchanged SolrInputDocument secondDoc = documents.get(1); assertThat(secondDoc.getFieldValue("id")).isEqualTo("2"); assertThat(secondDoc.getFieldValue("name")).isEqualTo("Regular Name"); assertThat(secondDoc.getFieldValue("description")).isEqualTo("Regular description"); } -} \ No newline at end of file +} diff --git a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceDirectTest.java b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceDirectTest.java index 418bcab..d1b9819 100644 --- a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceDirectTest.java +++ b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceDirectTest.java @@ -16,6 +16,12 @@ */ package org.apache.solr.mcp.server.indexing; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.response.UpdateResponse; import org.apache.solr.common.SolrInputDocument; @@ -26,29 +32,23 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class IndexingServiceDirectTest { - @Mock - private SolrClient solrClient; + @Mock private SolrClient solrClient; - @Mock - private UpdateResponse updateResponse; + @Mock private UpdateResponse updateResponse; private IndexingService indexingService; private IndexingDocumentCreator indexingDocumentCreator; + @BeforeEach void setUp() { - indexingDocumentCreator = new IndexingDocumentCreator(new XmlDocumentCreator(), - new CsvDocumentCreator(), - new JsonDocumentCreator()); + indexingDocumentCreator = + new IndexingDocumentCreator( + new XmlDocumentCreator(), + new CsvDocumentCreator(), + new JsonDocumentCreator()); indexingService = new IndexingService(solrClient, indexingDocumentCreator); } @@ -68,8 +68,7 @@ void testBatchIndexingErrorHandling() throws Exception { .thenThrow(new RuntimeException("Batch indexing failed")); // Individual document adds should succeed - when(solrClient.add(anyString(), any(SolrInputDocument.class))) - .thenReturn(updateResponse); + when(solrClient.add(anyString(), any(SolrInputDocument.class))).thenReturn(updateResponse); // Call the method under test int successCount = indexingService.indexDocuments("test_collection", documents); @@ -132,7 +131,8 @@ void testBatchIndexingPartialFailure() throws Exception { @Test void testIndexJsonDocumentsWithJsonString() throws Exception { // Test JSON string with multiple documents - String json = """ + String json = + """ [ { "id": "test001", @@ -149,7 +149,8 @@ void testIndexJsonDocumentsWithJsonString() throws Exception { // Create a spy on the indexingDocumentCreator and inject it into a new IndexingService IndexingDocumentCreator indexingDocumentCreatorSpy = spy(indexingDocumentCreator); - IndexingService indexingServiceWithSpy = new IndexingService(solrClient, indexingDocumentCreatorSpy); + IndexingService indexingServiceWithSpy = + new IndexingService(solrClient, indexingDocumentCreatorSpy); IndexingService indexingServiceSpy = spy(indexingServiceWithSpy); // Create mock documents that would be returned by createSchemalessDocuments @@ -168,7 +169,9 @@ void testIndexJsonDocumentsWithJsonString() throws Exception { mockDocuments.add(doc2); // Mock the createSchemalessDocuments method to return our mock documents - doReturn(mockDocuments).when(indexingDocumentCreatorSpy).createSchemalessDocumentsFromJson(json); + doReturn(mockDocuments) + .when(indexingDocumentCreatorSpy) + .createSchemalessDocumentsFromJson(json); // Mock the indexDocuments method that takes a collection and list of documents doReturn(2).when(indexingServiceSpy).indexDocuments(anyString(), anyList()); @@ -190,16 +193,22 @@ void testIndexJsonDocumentsWithJsonStringErrorHandling() throws Exception { // Create a spy on the indexingDocumentCreator and inject it into a new IndexingService IndexingDocumentCreator indexingDocumentCreatorSpy = spy(indexingDocumentCreator); - IndexingService indexingServiceWithSpy = new IndexingService(solrClient, indexingDocumentCreatorSpy); + IndexingService indexingServiceWithSpy = + new IndexingService(solrClient, indexingDocumentCreatorSpy); IndexingService indexingServiceSpy = spy(indexingServiceWithSpy); // Mock the createSchemalessDocuments method to throw an exception - doThrow(new DocumentProcessingException("Invalid JSON")).when(indexingDocumentCreatorSpy).createSchemalessDocumentsFromJson(invalidJson); + doThrow(new DocumentProcessingException("Invalid JSON")) + .when(indexingDocumentCreatorSpy) + .createSchemalessDocumentsFromJson(invalidJson); // Call the method under test and verify it throws an exception - DocumentProcessingException exception = assertThrows(DocumentProcessingException.class, () -> { - indexingServiceSpy.indexJsonDocuments("test_collection", invalidJson); - }); + DocumentProcessingException exception = + assertThrows( + DocumentProcessingException.class, + () -> { + indexingServiceSpy.indexJsonDocuments("test_collection", invalidJson); + }); // Verify the exception message assertTrue(exception.getMessage().contains("Invalid JSON")); diff --git a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceTest.java b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceTest.java index ca8eddb..0ca8b17 100644 --- a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceTest.java +++ b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceTest.java @@ -16,6 +16,15 @@ */ package org.apache.solr.mcp.server.indexing; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.request.CollectionAdminRequest; @@ -39,16 +48,6 @@ import org.springframework.context.annotation.Import; import org.testcontainers.containers.SolrContainer; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - @SpringBootTest @Import(TestcontainersConfiguration.class) class IndexingServiceTest { @@ -56,16 +55,11 @@ class IndexingServiceTest { private static boolean initialized = false; private static final String COLLECTION_NAME = "indexing_test_" + System.currentTimeMillis(); - @Autowired - private SolrContainer solrContainer; - @Autowired - private IndexingDocumentCreator indexingDocumentCreator; - @Autowired - private IndexingService indexingService; - @Autowired - private SearchService searchService; - @Autowired - private SolrClient solrClient; + @Autowired private SolrContainer solrContainer; + @Autowired private IndexingDocumentCreator indexingDocumentCreator; + @Autowired private IndexingService indexingService; + @Autowired private SearchService searchService; + @Autowired private SolrClient solrClient; @BeforeEach void setUp() throws Exception { @@ -75,27 +69,27 @@ void setUp() throws Exception { CsvDocumentCreator csvDocumentCreator = new CsvDocumentCreator(); JsonDocumentCreator jsonDocumentCreator = new JsonDocumentCreator(); - indexingDocumentCreator = new IndexingDocumentCreator(xmlDocumentCreator, - csvDocumentCreator, - jsonDocumentCreator); + indexingDocumentCreator = + new IndexingDocumentCreator( + xmlDocumentCreator, csvDocumentCreator, jsonDocumentCreator); indexingService = new IndexingService(solrClient, indexingDocumentCreator); searchService = new SearchService(solrClient); if (!initialized) { // Create collection - CollectionAdminRequest.Create createRequest = CollectionAdminRequest.createCollection( - COLLECTION_NAME, "_default", 1, 1); + CollectionAdminRequest.Create createRequest = + CollectionAdminRequest.createCollection(COLLECTION_NAME, "_default", 1, 1); createRequest.process(solrClient); initialized = true; } } - @Test void testCreateSchemalessDocumentsFromJson() throws Exception { // Test JSON string - String json = """ + String json = + """ [ { "id": "test001", @@ -112,7 +106,8 @@ void testCreateSchemalessDocumentsFromJson() throws Exception { """; // Create documents - List documents = indexingDocumentCreator.createSchemalessDocumentsFromJson(json); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromJson(json); // Verify documents were created correctly assertNotNull(documents); @@ -165,7 +160,8 @@ void testCreateSchemalessDocumentsFromJson() throws Exception { void testIndexJsonDocuments() throws Exception { // Test JSON string with multiple documents - String json = """ + String json = + """ [ { "id": "test002", @@ -192,7 +188,9 @@ void testIndexJsonDocuments() throws Exception { indexingService.indexJsonDocuments(COLLECTION_NAME, json); // Verify documents were indexed by searching for them - SearchResponse result = searchService.search(COLLECTION_NAME, "id:test002 OR id:test003", null, null, null, null, null); + SearchResponse result = + searchService.search( + COLLECTION_NAME, "id:test002 OR id:test003", null, null, null, null, null); assertNotNull(result); List> documents = result.documents(); @@ -275,7 +273,8 @@ void testIndexJsonDocuments() throws Exception { void testIndexJsonDocumentsWithNestedObjects() throws Exception { // Test JSON string with nested objects - String json = """ + String json = + """ [ { "id": "test004", @@ -296,7 +295,8 @@ void testIndexJsonDocumentsWithNestedObjects() throws Exception { indexingService.indexJsonDocuments(COLLECTION_NAME, json); // Verify documents were indexed by searching for them - SearchResponse result = searchService.search(COLLECTION_NAME, "id:test004", null, null, null, null, null); + SearchResponse result = + searchService.search(COLLECTION_NAME, "id:test004", null, null, null, null, null); assertNotNull(result); List> documents = result.documents(); @@ -344,7 +344,8 @@ void testIndexJsonDocumentsWithNestedObjects() throws Exception { void testSanitizeFieldName() throws Exception { // Test JSON string with field names that need sanitizing - String json = """ + String json = + """ [ { "id": "test005", @@ -360,7 +361,8 @@ void testSanitizeFieldName() throws Exception { indexingService.indexJsonDocuments(COLLECTION_NAME, json); // Verify documents were indexed with sanitized field names - SearchResponse result = searchService.search(COLLECTION_NAME, "id:test005", null, null, null, null, null); + SearchResponse result = + searchService.search(COLLECTION_NAME, "id:test005", null, null, null, null, null); assertNotNull(result); List> documents = result.documents(); @@ -398,7 +400,9 @@ void testSanitizeFieldName() throws Exception { assertNotNull(doc.get("multiple_underscores")); Object multipleUnderscoresValue = doc.get("multiple_underscores"); if (multipleUnderscoresValue instanceof List) { - assertEquals("Value with multiple underscores", ((List) multipleUnderscoresValue).getFirst()); + assertEquals( + "Value with multiple underscores", + ((List) multipleUnderscoresValue).getFirst()); } else { assertEquals("Value with multiple underscores", multipleUnderscoresValue); } @@ -408,7 +412,8 @@ void testSanitizeFieldName() throws Exception { void testDeeplyNestedJsonStructures() throws Exception { // Test JSON string with deeply nested objects (3+ levels) - String json = """ + String json = + """ [ { "id": "nested001", @@ -452,7 +457,8 @@ void testDeeplyNestedJsonStructures() throws Exception { indexingService.indexJsonDocuments(COLLECTION_NAME, json); // Verify documents were indexed by searching for them - SearchResponse result = searchService.search(COLLECTION_NAME, "id:nested001", null, null, null, null, null); + SearchResponse result = + searchService.search(COLLECTION_NAME, "id:nested001", null, null, null, null, null); assertNotNull(result); List> documents = result.documents(); @@ -463,15 +469,24 @@ void testDeeplyNestedJsonStructures() throws Exception { // Check that deeply nested fields were flattened with underscore prefix // Level 1 assertNotNull(doc.get("metadata_publication_publisher_name")); - assertEquals("Deep Nest Publishing", getFieldValue(doc, "metadata_publication_publisher_name")); + assertEquals( + "Deep Nest Publishing", getFieldValue(doc, "metadata_publication_publisher_name")); // Level 2 assertNotNull(doc.get("metadata_publication_publisher_location_city")); - assertEquals("Nestville", getFieldValue(doc, "metadata_publication_publisher_location_city")); + assertEquals( + "Nestville", getFieldValue(doc, "metadata_publication_publisher_location_city")); // Level 3 assertNotNull(doc.get("metadata_publication_publisher_location_coordinates_latitude")); - assertEquals(42.123, ((Number) getFieldValue(doc, "metadata_publication_publisher_location_coordinates_latitude")).doubleValue(), 0.001); + assertEquals( + 42.123, + ((Number) + getFieldValue( + doc, + "metadata_publication_publisher_location_coordinates_latitude")) + .doubleValue(), + 0.001); // Check other branches of the nested structure assertNotNull(doc.get("metadata_publication_edition_notes_condition")); @@ -493,7 +508,8 @@ private Object getFieldValue(Map doc, String fieldName) { void testSpecialCharactersInFieldNames() throws Exception { // Test JSON string with field names containing various special characters - String json = """ + String json = + """ [ { "id": "special_fields_001", @@ -529,7 +545,9 @@ void testSpecialCharactersInFieldNames() throws Exception { indexingService.indexJsonDocuments(COLLECTION_NAME, json); // Verify documents were indexed by searching for them - SearchResponse result = searchService.search(COLLECTION_NAME, "id:special_fields_001", null, null, null, null, null); + SearchResponse result = + searchService.search( + COLLECTION_NAME, "id:special_fields_001", null, null, null, null, null); assertNotNull(result); List> documents = result.documents(); @@ -574,7 +592,8 @@ void testSpecialCharactersInFieldNames() throws Exception { void testArraysOfObjects() throws Exception { // Test JSON string with arrays of objects - String json = """ + String json = + """ [ { "id": "array_objects_001", @@ -617,7 +636,9 @@ void testArraysOfObjects() throws Exception { indexingService.indexJsonDocuments(COLLECTION_NAME, json); // Verify documents were indexed by searching for them - SearchResponse result = searchService.search(COLLECTION_NAME, "id:array_objects_001", null, null, null, null, null); + SearchResponse result = + searchService.search( + COLLECTION_NAME, "id:array_objects_001", null, null, null, null, null); assertNotNull(result); List> documents = result.documents(); @@ -641,11 +662,13 @@ void testArraysOfObjects() throws Exception { // For arrays of objects, the IndexingService should flatten them with field names // that include the array name and the object field name - // We can't directly access the array elements, but we can check if the flattened fields exist + // We can't directly access the array elements, but we can check if the flattened fields + // exist // Check for flattened author fields // Note: The current implementation in IndexingService.java doesn't handle arrays of objects - // in a way that preserves the array structure. It skips object items in arrays (line 68-70). + // in a way that preserves the array structure. It skips object items in arrays (line + // 68-70). // This test is checking the current behavior, which may need improvement in the future. // Check for flattened review fields @@ -655,7 +678,8 @@ void testArraysOfObjects() throws Exception { @Test void testNonArrayJsonInput() throws Exception { // Test JSON string that is not an array but a single object - String json = """ + String json = + """ { "id": "single_object_001", "title": "Single Object Document", @@ -665,7 +689,8 @@ void testNonArrayJsonInput() throws Exception { """; // Create documents - List documents = indexingDocumentCreator.createSchemalessDocumentsFromJson(json); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromJson(json); // Verify no documents were created since input is not an array assertNotNull(documents); @@ -675,7 +700,8 @@ void testNonArrayJsonInput() throws Exception { @Test void testConvertJsonValueTypes() throws Exception { // Test JSON with different value types - String json = """ + String json = + """ [ { "id": "value_types_001", @@ -689,7 +715,8 @@ void testConvertJsonValueTypes() throws Exception { """; // Create documents - List documents = indexingDocumentCreator.createSchemalessDocumentsFromJson(json); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromJson(json); // Verify documents were created correctly assertNotNull(documents); @@ -710,7 +737,8 @@ void testConvertJsonValueTypes() throws Exception { void testDirectSanitizeFieldName() throws Exception { // Test sanitizing field names directly // Create a document with field names that need sanitizing - String json = """ + String json = + """ [ { "id": "field_names_001", @@ -726,7 +754,8 @@ void testDirectSanitizeFieldName() throws Exception { """; // Create documents - List documents = indexingDocumentCreator.createSchemalessDocumentsFromJson(json); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromJson(json); // Verify documents were created correctly assertNotNull(documents); @@ -746,16 +775,13 @@ void testDirectSanitizeFieldName() throws Exception { } } - @Nested @ExtendWith(MockitoExtension.class) class UnitTests { - @Mock - private SolrClient solrClient; + @Mock private SolrClient solrClient; - @Mock - private IndexingDocumentCreator indexingDocumentCreator; + @Mock private IndexingDocumentCreator indexingDocumentCreator; private IndexingService indexingService; @@ -785,14 +811,20 @@ void indexJsonDocuments_WithValidJson_ShouldIndexDocuments() throws Exception { } @Test - void indexJsonDocuments_WhenDocumentCreatorThrowsException_ShouldPropagateException() throws Exception { + void indexJsonDocuments_WhenDocumentCreatorThrowsException_ShouldPropagateException() + throws Exception { String invalidJson = "not valid json"; when(indexingDocumentCreator.createSchemalessDocumentsFromJson(invalidJson)) - .thenThrow(new org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException("Invalid JSON")); - - assertThrows(org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException.class, () -> { - indexingService.indexJsonDocuments("test_collection", invalidJson); - }); + .thenThrow( + new org.apache.solr.mcp.server.indexing.documentcreator + .DocumentProcessingException("Invalid JSON")); + + assertThrows( + org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException + .class, + () -> { + indexingService.indexJsonDocuments("test_collection", invalidJson); + }); verify(solrClient, never()).add(anyString(), any(Collection.class)); verify(solrClient, never()).commit(anyString()); } @@ -813,14 +845,20 @@ void indexCsvDocuments_WithValidCsv_ShouldIndexDocuments() throws Exception { } @Test - void indexCsvDocuments_WhenDocumentCreatorThrowsException_ShouldPropagateException() throws Exception { + void indexCsvDocuments_WhenDocumentCreatorThrowsException_ShouldPropagateException() + throws Exception { String invalidCsv = "malformed csv data"; when(indexingDocumentCreator.createSchemalessDocumentsFromCsv(invalidCsv)) - .thenThrow(new org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException("Invalid CSV")); - - assertThrows(org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException.class, () -> { - indexingService.indexCsvDocuments("test_collection", invalidCsv); - }); + .thenThrow( + new org.apache.solr.mcp.server.indexing.documentcreator + .DocumentProcessingException("Invalid CSV")); + + assertThrows( + org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException + .class, + () -> { + indexingService.indexCsvDocuments("test_collection", invalidCsv); + }); verify(solrClient, never()).add(anyString(), any(Collection.class)); verify(solrClient, never()).commit(anyString()); } @@ -841,14 +879,20 @@ void indexXmlDocuments_WithValidXml_ShouldIndexDocuments() throws Exception { } @Test - void indexXmlDocuments_WhenParserConfigurationFails_ShouldPropagateException() throws Exception { + void indexXmlDocuments_WhenParserConfigurationFails_ShouldPropagateException() + throws Exception { String xml = "xml"; when(indexingDocumentCreator.createSchemalessDocumentsFromXml(xml)) - .thenThrow(new org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException("Parser error")); - - assertThrows(org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException.class, () -> { - indexingService.indexXmlDocuments("test_collection", xml); - }); + .thenThrow( + new org.apache.solr.mcp.server.indexing.documentcreator + .DocumentProcessingException("Parser error")); + + assertThrows( + org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException + .class, + () -> { + indexingService.indexXmlDocuments("test_collection", xml); + }); verify(solrClient, never()).add(anyString(), any(Collection.class)); verify(solrClient, never()).commit(anyString()); } @@ -857,11 +901,16 @@ void indexXmlDocuments_WhenParserConfigurationFails_ShouldPropagateException() t void indexXmlDocuments_WhenSaxExceptionOccurs_ShouldPropagateException() throws Exception { String xml = ""; when(indexingDocumentCreator.createSchemalessDocumentsFromXml(xml)) - .thenThrow(new org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException("SAX parsing error")); - - assertThrows(org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException.class, () -> { - indexingService.indexXmlDocuments("test_collection", xml); - }); + .thenThrow( + new org.apache.solr.mcp.server.indexing.documentcreator + .DocumentProcessingException("SAX parsing error")); + + assertThrows( + org.apache.solr.mcp.server.indexing.documentcreator.DocumentProcessingException + .class, + () -> { + indexingService.indexXmlDocuments("test_collection", xml); + }); verify(solrClient, never()).add(anyString(), any(Collection.class)); verify(solrClient, never()).commit(anyString()); } @@ -911,7 +960,8 @@ void indexDocuments_WhenBatchFails_ShouldRetryIndividually() throws Exception { } @Test - void indexDocuments_WhenSomeIndividualDocumentsFail_ShouldIndexSuccessfulOnes() throws Exception { + void indexDocuments_WhenSomeIndividualDocumentsFail_ShouldIndexSuccessfulOnes() + throws Exception { List docs = createMockDocuments(3); when(solrClient.add(eq("test_collection"), any(List.class))) @@ -950,9 +1000,11 @@ void indexDocuments_WhenCommitFails_ShouldPropagateException() throws Exception when(solrClient.add(eq("test_collection"), any(Collection.class))).thenReturn(null); when(solrClient.commit("test_collection")).thenThrow(new IOException("Commit failed")); - assertThrows(IOException.class, () -> { - indexingService.indexDocuments("test_collection", docs); - }); + assertThrows( + IOException.class, + () -> { + indexingService.indexDocuments("test_collection", docs); + }); verify(solrClient).add(eq("test_collection"), any(Collection.class)); verify(solrClient).commit("test_collection"); } @@ -967,14 +1019,16 @@ void indexDocuments_ShouldBatchCorrectly() throws Exception { assertEquals(1000, result); - ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); + ArgumentCaptor> captor = + ArgumentCaptor.forClass(Collection.class); verify(solrClient).add(eq("test_collection"), captor.capture()); assertEquals(1000, captor.getValue().size()); verify(solrClient).commit("test_collection"); } @Test - void indexJsonDocuments_WhenSolrClientThrowsException_ShouldPropagateException() throws Exception { + void indexJsonDocuments_WhenSolrClientThrowsException_ShouldPropagateException() + throws Exception { String json = "[{\"id\":\"1\"}]"; List mockDocs = createMockDocuments(1); when(indexingDocumentCreator.createSchemalessDocumentsFromJson(json)).thenReturn(mockDocs); @@ -991,7 +1045,8 @@ void indexJsonDocuments_WhenSolrClientThrowsException_ShouldPropagateException() } @Test - void indexCsvDocuments_WhenSolrClientThrowsIOException_ShouldPropagateException() throws Exception { + void indexCsvDocuments_WhenSolrClientThrowsIOException_ShouldPropagateException() + throws Exception { String csv = "id,title\n1,Test"; List mockDocs = createMockDocuments(1); when(indexingDocumentCreator.createSchemalessDocumentsFromCsv(csv)).thenReturn(mockDocs); diff --git a/src/test/java/org/apache/solr/mcp/server/indexing/XmlIndexingTest.java b/src/test/java/org/apache/solr/mcp/server/indexing/XmlIndexingTest.java index c43f2f9..a8a6b63 100644 --- a/src/test/java/org/apache/solr/mcp/server/indexing/XmlIndexingTest.java +++ b/src/test/java/org/apache/solr/mcp/server/indexing/XmlIndexingTest.java @@ -16,6 +16,10 @@ */ package org.apache.solr.mcp.server.indexing; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.mcp.server.indexing.documentcreator.IndexingDocumentCreator; import org.junit.jupiter.api.Test; @@ -23,29 +27,24 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - /** * Test class for XML indexing functionality in IndexingService. * - *

This test verifies that the IndexingService can correctly parse XML data - * and convert it into SolrInputDocument objects using the schema-less approach.

+ *

This test verifies that the IndexingService can correctly parse XML data and convert it into + * SolrInputDocument objects using the schema-less approach. */ @SpringBootTest @TestPropertySource(locations = "classpath:application.properties") class XmlIndexingTest { - @Autowired - private IndexingDocumentCreator indexingDocumentCreator; + @Autowired private IndexingDocumentCreator indexingDocumentCreator; @Test void testCreateSchemalessDocumentsFromXmlSingleDocument() throws Exception { // Given - String xmlData = """ + String xmlData = + """ A Game of Thrones @@ -59,7 +58,8 @@ void testCreateSchemalessDocumentsFromXmlSingleDocument() throws Exception { """; // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); // Then assertThat(documents).hasSize(1); @@ -78,7 +78,8 @@ void testCreateSchemalessDocumentsFromXmlSingleDocument() throws Exception { void testCreateSchemalessDocumentsFromXmlMultipleDocuments() throws Exception { // Given - String xmlData = """ + String xmlData = + """ A Game of Thrones @@ -99,7 +100,8 @@ void testCreateSchemalessDocumentsFromXmlMultipleDocuments() throws Exception { """; // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); // Then assertThat(documents).hasSize(3); @@ -130,7 +132,8 @@ void testCreateSchemalessDocumentsFromXmlMultipleDocuments() throws Exception { void testCreateSchemalessDocumentsFromXmlWithAttributes() throws Exception { // Given - String xmlData = """ + String xmlData = + """ Smartphone 599.99 @@ -139,7 +142,8 @@ void testCreateSchemalessDocumentsFromXmlWithAttributes() throws Exception { """; // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); // Then assertThat(documents).hasSize(1); @@ -152,14 +156,16 @@ void testCreateSchemalessDocumentsFromXmlWithAttributes() throws Exception { assertThat(doc.getFieldValue("product_price_currency_attr")).isEqualTo("USD"); assertThat(doc.getFieldValue("product_name")).isEqualTo("Smartphone"); assertThat(doc.getFieldValue("product_price")).isEqualTo("599.99"); - assertThat(doc.getFieldValue("product_description")).isEqualTo("Latest smartphone with advanced features"); + assertThat(doc.getFieldValue("product_description")) + .isEqualTo("Latest smartphone with advanced features"); } @Test void testCreateSchemalessDocumentsFromXmlWithEmptyValues() throws Exception { // Given - String xmlData = """ + String xmlData = + """ Product One @@ -175,7 +181,8 @@ void testCreateSchemalessDocumentsFromXmlWithEmptyValues() throws Exception { """; // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); // Then assertThat(documents).hasSize(2); @@ -184,13 +191,15 @@ void testCreateSchemalessDocumentsFromXmlWithEmptyValues() throws Exception { SolrInputDocument firstDoc = documents.getFirst(); assertThat(firstDoc.getFieldValue("id_attr")).isEqualTo("1"); assertThat(firstDoc.getFieldValue("item_name")).isEqualTo("Product One"); - assertThat(firstDoc.getFieldValue("item_description")).isNull(); // Empty element should not be indexed + assertThat(firstDoc.getFieldValue("item_description")) + .isNull(); // Empty element should not be indexed assertThat(firstDoc.getFieldValue("item_price")).isEqualTo("19.99"); // Second document should skip empty name SolrInputDocument secondDoc = documents.get(1); assertThat(secondDoc.getFieldValue("id_attr")).isEqualTo("2"); - assertThat(secondDoc.getFieldValue("item_name")).isNull(); // Empty element should not be indexed + assertThat(secondDoc.getFieldValue("item_name")) + .isNull(); // Empty element should not be indexed assertThat(secondDoc.getFieldValue("item_description")).isEqualTo("Product with no name"); assertThat(secondDoc.getFieldValue("item_price")).isEqualTo("29.99"); } @@ -199,7 +208,8 @@ void testCreateSchemalessDocumentsFromXmlWithEmptyValues() throws Exception { void testCreateSchemalessDocumentsFromXmlWithRepeatedElements() throws Exception { // Given - String xmlData = """ + String xmlData = + """ Programming Book John Doe @@ -216,7 +226,8 @@ void testCreateSchemalessDocumentsFromXmlWithRepeatedElements() throws Exception """; // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); // Then assertThat(documents).hasSize(1); @@ -236,11 +247,12 @@ void testCreateSchemalessDocumentsFromXmlWithRepeatedElements() throws Exception void testCreateSchemalessDocumentsFromXmlMixedContent() throws Exception { // Given - String xmlData = """ + String xmlData = + """

Mixed Content Example - This is some text content with + This is some text content with emphasized text and more content here. @@ -249,7 +261,8 @@ void testCreateSchemalessDocumentsFromXmlMixedContent() throws Exception { """; // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlData); // Then assertThat(documents).hasSize(1); @@ -267,7 +280,8 @@ void testCreateSchemalessDocumentsFromXmlMixedContent() throws Exception { void testCreateSchemalessDocumentsFromXmlWithMalformedXml() { // Given - String malformedXml = """ + String malformedXml = + """ Incomplete Book <author>John Doe</author> @@ -275,7 +289,10 @@ void testCreateSchemalessDocumentsFromXmlWithMalformedXml() { """; // When/Then - assertThatThrownBy(() -> indexingDocumentCreator.createSchemalessDocumentsFromXml(malformedXml)) + assertThatThrownBy( + () -> + indexingDocumentCreator.createSchemalessDocumentsFromXml( + malformedXml)) .isInstanceOf(RuntimeException.class); } @@ -283,7 +300,8 @@ void testCreateSchemalessDocumentsFromXmlWithMalformedXml() { void testCreateSchemalessDocumentsFromXmlWithInvalidCharacters() { // Given - String invalidXml = """ + String invalidXml = + """ <book> <title>Book with invalid character: \u0000 John Doe @@ -291,7 +309,8 @@ void testCreateSchemalessDocumentsFromXmlWithInvalidCharacters() { """; // When/Then - assertThatThrownBy(() -> indexingDocumentCreator.createSchemalessDocumentsFromXml(invalidXml)) + assertThatThrownBy( + () -> indexingDocumentCreator.createSchemalessDocumentsFromXml(invalidXml)) .isInstanceOf(RuntimeException.class); } @@ -299,7 +318,8 @@ void testCreateSchemalessDocumentsFromXmlWithInvalidCharacters() { void testCreateSchemalessDocumentsFromXmlWithDoctype() { // Given - String xmlWithDoctype = """ + String xmlWithDoctype = + """ @@ -313,7 +333,10 @@ void testCreateSchemalessDocumentsFromXmlWithDoctype() { """; // When/Then - Should fail due to XXE protection - assertThatThrownBy(() -> indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlWithDoctype)) + assertThatThrownBy( + () -> + indexingDocumentCreator.createSchemalessDocumentsFromXml( + xmlWithDoctype)) .isInstanceOf(RuntimeException.class); } @@ -321,7 +344,8 @@ void testCreateSchemalessDocumentsFromXmlWithDoctype() { void testCreateSchemalessDocumentsFromXmlWithExternalEntity() { // Given - String xmlWithExternalEntity = """ + String xmlWithExternalEntity = + """ @@ -333,7 +357,10 @@ void testCreateSchemalessDocumentsFromXmlWithExternalEntity() { """; // When/Then - Should fail due to XXE protection - assertThatThrownBy(() -> indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlWithExternalEntity)) + assertThatThrownBy( + () -> + indexingDocumentCreator.createSchemalessDocumentsFromXml( + xmlWithExternalEntity)) .isInstanceOf(RuntimeException.class); } @@ -362,7 +389,8 @@ void testCreateSchemalessDocumentsFromXmlWithWhitespaceOnlyInput() { // Given // When/Then - assertThatThrownBy(() -> indexingDocumentCreator.createSchemalessDocumentsFromXml(" \n\t ")) + assertThatThrownBy( + () -> indexingDocumentCreator.createSchemalessDocumentsFromXml(" \n\t ")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("XML input cannot be null or empty"); } @@ -376,7 +404,8 @@ void testCreateSchemalessDocumentsFromXmlWithLargeDocument() { largeXml.append(""); // Add enough data to exceed the 10MB limit - String bookTemplate = """ + String bookTemplate = + """ %s %s @@ -391,7 +420,10 @@ void testCreateSchemalessDocumentsFromXmlWithLargeDocument() { largeXml.append(""); // When/Then - assertThatThrownBy(() -> indexingDocumentCreator.createSchemalessDocumentsFromXml(largeXml.toString())) + assertThatThrownBy( + () -> + indexingDocumentCreator.createSchemalessDocumentsFromXml( + largeXml.toString())) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("XML document too large"); } @@ -400,7 +432,8 @@ void testCreateSchemalessDocumentsFromXmlWithLargeDocument() { void testCreateSchemalessDocumentsFromXmlWithComplexNestedStructure() throws Exception { // Given - String complexXml = """ + String complexXml = + """
Smartphone @@ -428,7 +461,8 @@ void testCreateSchemalessDocumentsFromXmlWithComplexNestedStructure() throws Exc """; // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromXml(complexXml); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromXml(complexXml); // Then assertThat(documents).hasSize(1); @@ -441,17 +475,24 @@ void testCreateSchemalessDocumentsFromXmlWithComplexNestedStructure() throws Exc // Verify nested structure flattening assertThat(doc.getFieldValue("product_details_name_lang_attr")).isNotNull(); - assertThat(doc.getFieldValue("product_details_specifications_screen_size_attr")).isEqualTo("6.1"); - assertThat(doc.getFieldValue("product_details_specifications_screen_type_attr")).isEqualTo("OLED"); - assertThat(doc.getFieldValue("product_details_specifications_screen")).isEqualTo("Full HD+"); + assertThat(doc.getFieldValue("product_details_specifications_screen_size_attr")) + .isEqualTo("6.1"); + assertThat(doc.getFieldValue("product_details_specifications_screen_type_attr")) + .isEqualTo("OLED"); + assertThat(doc.getFieldValue("product_details_specifications_screen")) + .isEqualTo("Full HD+"); // Verify multiple similar elements - assertThat(doc.getFieldValue("product_details_specifications_camera_type_attr")).isNotNull(); - assertThat(doc.getFieldValue("product_details_specifications_camera_resolution_attr")).isNotNull(); + assertThat(doc.getFieldValue("product_details_specifications_camera_type_attr")) + .isNotNull(); + assertThat(doc.getFieldValue("product_details_specifications_camera_resolution_attr")) + .isNotNull(); // Verify deeply nested elements - assertThat(doc.getFieldValue("product_details_specifications_storage_internal")).isEqualTo("128GB"); - assertThat(doc.getFieldValue("product_details_specifications_storage_expandable")).isEqualTo("Yes"); + assertThat(doc.getFieldValue("product_details_specifications_storage_internal")) + .isEqualTo("128GB"); + assertThat(doc.getFieldValue("product_details_specifications_storage_expandable")) + .isEqualTo("Yes"); // Verify pricing and availability assertThat(doc.getFieldValue("product_pricing_currency_attr")).isEqualTo("USD"); @@ -464,7 +505,8 @@ void testCreateSchemalessDocumentsFromXmlWithComplexNestedStructure() throws Exc void testFieldNameSanitization() throws Exception { // Given - String xmlWithSpecialChars = """ + String xmlWithSpecialChars = + """ Test Product 99.99 @@ -476,7 +518,8 @@ void testFieldNameSanitization() throws Exception { """; // When - List documents = indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlWithSpecialChars); + List documents = + indexingDocumentCreator.createSchemalessDocumentsFromXml(xmlWithSpecialChars); // Then assertThat(documents).hasSize(1); @@ -488,8 +531,9 @@ void testFieldNameSanitization() throws Exception { assertThat(doc.getFieldValue("product_data_product_name")).isEqualTo("Test Product"); assertThat(doc.getFieldValue("product_data_price_usd")).isEqualTo("99.99"); assertThat(doc.getFieldValue("product_data_category_type")).isEqualTo("electronics"); - assertThat(doc.getFieldValue("product_data_field_with_multiple_underscores")).isEqualTo("value"); + assertThat(doc.getFieldValue("product_data_field_with_multiple_underscores")) + .isEqualTo("value"); assertThat(doc.getFieldValue("product_data_field_with_dashes")).isEqualTo("dashed value"); assertThat(doc.getFieldValue("product_data_uppercase_field")).isEqualTo("uppercase value"); } -} \ No newline at end of file +} diff --git a/src/test/java/org/apache/solr/mcp/server/metadata/CollectionServiceIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/metadata/CollectionServiceIntegrationTest.java index a12be4f..5f9261c 100644 --- a/src/test/java/org/apache/solr/mcp/server/metadata/CollectionServiceIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/metadata/CollectionServiceIntegrationTest.java @@ -16,6 +16,9 @@ */ package org.apache.solr.mcp.server.metadata; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.mcp.server.TestcontainersConfiguration; @@ -25,19 +28,13 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - @SpringBootTest @Import(TestcontainersConfiguration.class) class CollectionServiceIntegrationTest { private static final String TEST_COLLECTION = "test_collection"; - @Autowired - private CollectionService collectionService; - @Autowired - private SolrClient solrClient; + @Autowired private CollectionService collectionService; + @Autowired private SolrClient solrClient; private static boolean initialized = false; @BeforeEach @@ -46,7 +43,8 @@ void setupCollection() throws Exception { if (!initialized) { // Create a test collection using the container's connection details // Create a collection for testing - CollectionAdminRequest.Create createRequest = CollectionAdminRequest.createCollection(TEST_COLLECTION, "_default", 1, 1); + CollectionAdminRequest.Create createRequest = + CollectionAdminRequest.createCollection(TEST_COLLECTION, "_default", 1, 1); createRequest.process(solrClient); // Verify collection was created successfully @@ -71,11 +69,17 @@ void testListCollections() { assertFalse(collections.isEmpty(), "Collections list should not be empty"); // Check if the test collection exists (either as exact name or as shard) - boolean testCollectionExists = collections.contains(TEST_COLLECTION) || - collections.stream().anyMatch(col -> col.startsWith(TEST_COLLECTION + "_shard")); - assertTrue(testCollectionExists, - "Collections should contain the test collection: " + TEST_COLLECTION + - " (found: " + collections + ")"); + boolean testCollectionExists = + collections.contains(TEST_COLLECTION) + || collections.stream() + .anyMatch(col -> col.startsWith(TEST_COLLECTION + "_shard")); + assertTrue( + testCollectionExists, + "Collections should contain the test collection: " + + TEST_COLLECTION + + " (found: " + + collections + + ")"); // Verify collection names are not null or empty for (String collection : collections) { @@ -84,18 +88,20 @@ void testListCollections() { } // Verify expected collection characteristics - assertEquals(collections.size(), collections.stream().distinct().count(), + assertEquals( + collections.size(), + collections.stream().distinct().count(), "Collection names should be unique"); // Verify that collections follow expected naming patterns for (String collection : collections) { // Collection names should either be simple names or shard names - assertTrue(collection.matches("^[a-zA-Z0-9_]+(_shard\\d+_replica_n\\d+)?$"), + assertTrue( + collection.matches("^[a-zA-Z0-9_]+(_shard\\d+_replica_n\\d+)?$"), "Collection name should follow expected pattern: " + collection); } } - @Test void testGetCollectionStats() throws Exception { // Test getting collection stats @@ -124,24 +130,26 @@ void testGetCollectionStats() throws Exception { // Verify timestamp is recent (within last 10 seconds) long currentTime = System.currentTimeMillis(); long timestampTime = metrics.timestamp().getTime(); - assertTrue(currentTime - timestampTime < 10000, + assertTrue( + currentTime - timestampTime < 10000, "Timestamp should be recent (within 10 seconds)"); // Verify optional stats (cache and handler stats may be null, which is acceptable) if (metrics.cacheStats() != null) { CacheStats cacheStats = metrics.cacheStats(); // Verify at least one cache type exists if cache stats are present - assertTrue(cacheStats.queryResultCache() != null || - cacheStats.documentCache() != null || - cacheStats.filterCache() != null, + assertTrue( + cacheStats.queryResultCache() != null + || cacheStats.documentCache() != null + || cacheStats.filterCache() != null, "At least one cache type should be present if cache stats exist"); } if (metrics.handlerStats() != null) { HandlerStats handlerStats = metrics.handlerStats(); // Verify at least one handler type exists if handler stats are present - assertTrue(handlerStats.selectHandler() != null || - handlerStats.updateHandler() != null, + assertTrue( + handlerStats.selectHandler() != null || handlerStats.updateHandler() != null, "At least one handler type should be present if handler stats exist"); } } @@ -161,7 +169,8 @@ void testCheckHealthHealthy() { // Verify response time assertNotNull(status.responseTime(), "Response time should not be null"); assertTrue(status.responseTime() >= 0, "Response time should be non-negative"); - assertTrue(status.responseTime() < 30000, "Response time should be reasonable (< 30 seconds)"); + assertTrue( + status.responseTime() < 30000, "Response time should be reasonable (< 30 seconds)"); // Verify document count assertNotNull(status.totalDocuments(), "Total documents should not be null"); @@ -171,7 +180,8 @@ void testCheckHealthHealthy() { assertNotNull(status.lastChecked(), "Last checked timestamp should not be null"); long currentTime = System.currentTimeMillis(); long lastCheckedTime = status.lastChecked().getTime(); - assertTrue(currentTime - lastCheckedTime < 5000, + assertTrue( + currentTime - lastCheckedTime < 5000, "Last checked timestamp should be very recent (within 5 seconds)"); // Verify no error message for healthy collection @@ -180,7 +190,8 @@ void testCheckHealthHealthy() { // Verify string representation contains meaningful information String statusString = status.toString(); if (statusString != null) { - assertTrue(statusString.contains("healthy") || statusString.contains("true"), + assertTrue( + statusString.contains("healthy") || statusString.contains("true"), "Status string should indicate healthy state"); } } @@ -202,29 +213,38 @@ void testCheckHealthUnhealthy() { assertNotNull(status.lastChecked(), "Last checked timestamp should not be null"); long currentTime = System.currentTimeMillis(); long lastCheckedTime = status.lastChecked().getTime(); - assertTrue(currentTime - lastCheckedTime < 5000, + assertTrue( + currentTime - lastCheckedTime < 5000, "Last checked timestamp should be very recent (within 5 seconds)"); // Verify error message - assertNotNull(status.errorMessage(), "Error message should not be null for unhealthy collection"); - assertFalse(status.errorMessage().trim().isEmpty(), + assertNotNull( + status.errorMessage(), "Error message should not be null for unhealthy collection"); + assertFalse( + status.errorMessage().trim().isEmpty(), "Error message should not be empty for unhealthy collection"); // Verify that performance metrics are null for unhealthy collection assertNull(status.responseTime(), "Response time should be null for unhealthy collection"); - assertNull(status.totalDocuments(), "Total documents should be null for unhealthy collection"); + assertNull( + status.totalDocuments(), "Total documents should be null for unhealthy collection"); // Verify error message contains meaningful information String errorMessage = status.errorMessage().toLowerCase(); - assertTrue(errorMessage.contains("collection") || errorMessage.contains("not found") || - errorMessage.contains("error") || errorMessage.contains("fail"), + assertTrue( + errorMessage.contains("collection") + || errorMessage.contains("not found") + || errorMessage.contains("error") + || errorMessage.contains("fail"), "Error message should contain meaningful error information"); // Verify string representation indicates unhealthy state String statusString = status.toString(); if (statusString != null) { - assertTrue(statusString.contains("false") || statusString.contains("unhealthy") || - statusString.contains("error"), + assertTrue( + statusString.contains("false") + || statusString.contains("unhealthy") + || statusString.contains("error"), "Status string should indicate unhealthy state"); } } @@ -232,22 +252,25 @@ void testCheckHealthUnhealthy() { @Test void testCollectionNameExtraction() { // Test collection name extraction functionality - assertEquals(TEST_COLLECTION, + assertEquals( + TEST_COLLECTION, collectionService.extractCollectionName(TEST_COLLECTION), "Regular collection name should be returned as-is"); - assertEquals("films", + assertEquals( + "films", collectionService.extractCollectionName("films_shard1_replica_n1"), "Shard name should be extracted to base collection name"); - assertEquals("products", + assertEquals( + "products", collectionService.extractCollectionName("products_shard2_replica_n3"), "Complex shard name should be extracted correctly"); - assertNull(collectionService.extractCollectionName(null), - "Null input should return null"); + assertNull(collectionService.extractCollectionName(null), "Null input should return null"); - assertEquals("", + assertEquals( + "", collectionService.extractCollectionName(""), "Empty string should return empty string"); } 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 b29781f..02fccd0 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 @@ -16,6 +16,16 @@ */ package org.apache.solr.mcp.server.metadata; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; @@ -31,34 +41,18 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - @ExtendWith(MockitoExtension.class) class CollectionServiceTest { - @Mock - private SolrClient solrClient; + @Mock private SolrClient solrClient; - @Mock - private CloudSolrClient cloudSolrClient; + @Mock private CloudSolrClient cloudSolrClient; - @Mock - private QueryResponse queryResponse; + @Mock private QueryResponse queryResponse; - @Mock - private LukeResponse lukeResponse; + @Mock private LukeResponse lukeResponse; - @Mock - private SolrPingResponse pingResponse; + @Mock private SolrPingResponse pingResponse; private CollectionService collectionService; @@ -115,7 +109,8 @@ void extractCollectionName_WithShardName_ShouldExtractCollectionName() { @Test void extractCollectionName_WithMultipleShards_ShouldExtractCorrectly() { // Given & When & Then - assertEquals("products", collectionService.extractCollectionName("products_shard2_replica_n3")); + assertEquals( + "products", collectionService.extractCollectionName("products_shard2_replica_n3")); assertEquals("users", collectionService.extractCollectionName("users_shard5_replica_n10")); } @@ -150,7 +145,8 @@ void extractCollectionName_WithEmptyString_ShouldReturnEmptyString() { } @Test - void extractCollectionName_WithCollectionNameContainingUnderscore_ShouldOnlyExtractBeforeShard() { + void + extractCollectionName_WithCollectionNameContainingUnderscore_ShouldOnlyExtractBeforeShard() { // Given - collection name itself contains underscore String complexName = "my_complex_collection_shard1_replica_n1"; @@ -179,7 +175,10 @@ void extractCollectionName_WithShardInMiddleOfName_ShouldExtractCorrectly() { String result = collectionService.extractCollectionName(name); // Then - assertEquals("resharding_tasks", result, "Should not extract when '_shard' is not followed by number"); + assertEquals( + "resharding_tasks", + result, + "Should not extract when '_shard' is not followed by number"); } @Test @@ -296,8 +295,7 @@ void checkHealth_WithSlowResponse_ShouldCaptureResponseTime() throws Exception { @Test void checkHealth_IOException() throws Exception { - when(solrClient.ping("error_collection")) - .thenThrow(new IOException("Network error")); + when(solrClient.ping("error_collection")).thenThrow(new IOException("Network error")); SolrHealthStatus result = collectionService.checkHealth("error_collection"); @@ -383,8 +381,10 @@ void getCollectionStats_NotFound() { CollectionService spyService = spy(collectionService); doReturn(Collections.emptyList()).when(spyService).listCollections(); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> spyService.getCollectionStats("non_existent")); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> spyService.getCollectionStats("non_existent")); assertTrue(exception.getMessage().contains("Collection not found: non_existent")); } @@ -395,7 +395,8 @@ void validateCollectionExists() throws Exception { List collections = Arrays.asList("collection1", "films_shard1_replica_n1"); doReturn(collections).when(spyService).listCollections(); - Method method = CollectionService.class.getDeclaredMethod("validateCollectionExists", String.class); + Method method = + CollectionService.class.getDeclaredMethod("validateCollectionExists", String.class); method.setAccessible(true); assertTrue((boolean) method.invoke(spyService, "collection1")); @@ -408,7 +409,8 @@ void validateCollectionExists_WithException() throws Exception { CollectionService spyService = spy(collectionService); doReturn(Collections.emptyList()).when(spyService).listCollections(); - Method method = CollectionService.class.getDeclaredMethod("validateCollectionExists", String.class); + Method method = + CollectionService.class.getDeclaredMethod("validateCollectionExists", String.class); method.setAccessible(true); assertFalse((boolean) method.invoke(spyService, "any_collection")); @@ -467,8 +469,7 @@ void getCacheMetrics_IOException() throws Exception { CollectionService spyService = spy(collectionService); doReturn(Arrays.asList("test_collection")).when(spyService).listCollections(); - when(solrClient.request(any(SolrRequest.class))) - .thenThrow(new IOException("IO Error")); + when(solrClient.request(any(SolrRequest.class))).thenThrow(new IOException("IO Error")); CacheStats result = spyService.getCacheMetrics("test_collection"); @@ -505,7 +506,8 @@ void getCacheMetrics_WithShardName() throws Exception { @Test void extractCacheStats() throws Exception { NamedList mbeans = createMockCacheData(); - Method method = CollectionService.class.getDeclaredMethod("extractCacheStats", NamedList.class); + Method method = + CollectionService.class.getDeclaredMethod("extractCacheStats", NamedList.class); method.setAccessible(true); CacheStats result = (CacheStats) method.invoke(collectionService, mbeans); @@ -518,7 +520,8 @@ void extractCacheStats() throws Exception { @Test void extractCacheStats_AllCacheTypes() throws Exception { NamedList mbeans = createCompleteMockCacheData(); - Method method = CollectionService.class.getDeclaredMethod("extractCacheStats", NamedList.class); + Method method = + CollectionService.class.getDeclaredMethod("extractCacheStats", NamedList.class); method.setAccessible(true); CacheStats result = (CacheStats) method.invoke(collectionService, mbeans); @@ -533,7 +536,8 @@ void extractCacheStats_NullCacheCategory() throws Exception { NamedList mbeans = new NamedList<>(); mbeans.add("CACHE", null); - Method method = CollectionService.class.getDeclaredMethod("extractCacheStats", NamedList.class); + Method method = + CollectionService.class.getDeclaredMethod("extractCacheStats", NamedList.class); method.setAccessible(true); CacheStats result = (CacheStats) method.invoke(collectionService, mbeans); @@ -546,18 +550,16 @@ void extractCacheStats_NullCacheCategory() throws Exception { @Test void isCacheStatsEmpty() throws Exception { - Method method = CollectionService.class.getDeclaredMethod("isCacheStatsEmpty", CacheStats.class); + Method method = + CollectionService.class.getDeclaredMethod("isCacheStatsEmpty", CacheStats.class); method.setAccessible(true); CacheStats emptyStats = new CacheStats(null, null, null); assertTrue((boolean) method.invoke(collectionService, emptyStats)); assertTrue((boolean) method.invoke(collectionService, (CacheStats) null)); - CacheStats nonEmptyStats = new CacheStats( - new CacheInfo(100L, null, null, null, null, null), - null, - null - ); + CacheStats nonEmptyStats = + new CacheStats(new CacheInfo(100L, null, null, null, null, null), null, null); assertFalse((boolean) method.invoke(collectionService, nonEmptyStats)); } @@ -613,8 +615,7 @@ void getHandlerMetrics_IOException() throws Exception { CollectionService spyService = spy(collectionService); doReturn(Arrays.asList("test_collection")).when(spyService).listCollections(); - when(solrClient.request(any(SolrRequest.class))) - .thenThrow(new IOException("IO Error")); + when(solrClient.request(any(SolrRequest.class))).thenThrow(new IOException("IO Error")); HandlerStats result = spyService.getHandlerMetrics("test_collection"); @@ -651,7 +652,8 @@ void getHandlerMetrics_WithShardName() throws Exception { @Test void extractHandlerStats() throws Exception { NamedList mbeans = createMockHandlerData(); - Method method = CollectionService.class.getDeclaredMethod("extractHandlerStats", NamedList.class); + Method method = + CollectionService.class.getDeclaredMethod("extractHandlerStats", NamedList.class); method.setAccessible(true); HandlerStats result = (HandlerStats) method.invoke(collectionService, mbeans); @@ -663,7 +665,8 @@ void extractHandlerStats() throws Exception { @Test void extractHandlerStats_BothHandlers() throws Exception { NamedList mbeans = createCompleteHandlerData(); - Method method = CollectionService.class.getDeclaredMethod("extractHandlerStats", NamedList.class); + Method method = + CollectionService.class.getDeclaredMethod("extractHandlerStats", NamedList.class); method.setAccessible(true); HandlerStats result = (HandlerStats) method.invoke(collectionService, mbeans); @@ -679,7 +682,8 @@ void extractHandlerStats_NullHandlerCategory() throws Exception { NamedList mbeans = new NamedList<>(); mbeans.add("QUERYHANDLER", null); - Method method = CollectionService.class.getDeclaredMethod("extractHandlerStats", NamedList.class); + Method method = + CollectionService.class.getDeclaredMethod("extractHandlerStats", NamedList.class); method.setAccessible(true); HandlerStats result = (HandlerStats) method.invoke(collectionService, mbeans); @@ -691,17 +695,17 @@ void extractHandlerStats_NullHandlerCategory() throws Exception { @Test void isHandlerStatsEmpty() throws Exception { - Method method = CollectionService.class.getDeclaredMethod("isHandlerStatsEmpty", HandlerStats.class); + Method method = + CollectionService.class.getDeclaredMethod( + "isHandlerStatsEmpty", HandlerStats.class); method.setAccessible(true); HandlerStats emptyStats = new HandlerStats(null, null); assertTrue((boolean) method.invoke(collectionService, emptyStats)); assertTrue((boolean) method.invoke(collectionService, (HandlerStats) null)); - HandlerStats nonEmptyStats = new HandlerStats( - new HandlerInfo(100L, null, null, null, null, null), - null - ); + HandlerStats nonEmptyStats = + new HandlerStats(new HandlerInfo(100L, null, null, null, null, null), null); assertFalse((boolean) method.invoke(collectionService, nonEmptyStats)); } @@ -743,7 +747,8 @@ void listCollections_CloudClient_NullCollections() throws Exception { @Test void listCollections_CloudClient_Error() throws Exception { CloudSolrClient cloudClient = mock(CloudSolrClient.class); - when(cloudClient.request(any(), any())).thenThrow(new SolrServerException("Connection error")); + when(cloudClient.request(any(), any())) + .thenThrow(new SolrServerException("Connection error")); CollectionService service = new CollectionService(cloudClient); List result = service.listCollections(); @@ -904,4 +909,4 @@ private NamedList createCompleteHandlerData() { return mbeans; } -} \ No newline at end of file +} diff --git a/src/test/java/org/apache/solr/mcp/server/metadata/CollectionUtilsTest.java b/src/test/java/org/apache/solr/mcp/server/metadata/CollectionUtilsTest.java index 558f9dc..1e0ba34 100644 --- a/src/test/java/org/apache/solr/mcp/server/metadata/CollectionUtilsTest.java +++ b/src/test/java/org/apache/solr/mcp/server/metadata/CollectionUtilsTest.java @@ -16,18 +16,17 @@ */ package org.apache.solr.mcp.server.metadata; -import org.apache.solr.common.util.NamedList; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import java.math.BigDecimal; import java.math.BigInteger; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import org.apache.solr.common.util.NamedList; +import org.junit.jupiter.api.Test; /** - * Comprehensive test suite for the Utils utility class. - * Tests all public methods and edge cases for type-safe value extraction from Solr NamedList objects. + * Comprehensive test suite for the Utils utility class. Tests all public methods and edge cases for + * type-safe value extraction from Solr NamedList objects. */ class CollectionUtilsTest { @@ -309,4 +308,4 @@ void testGetInteger_withZeroValue() { assertEquals(0, CollectionUtils.getInteger(namedList, "zeroKey")); } -} \ No newline at end of file +} diff --git a/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceIntegrationTest.java index 3b61514..e49e3c7 100644 --- a/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceIntegrationTest.java +++ b/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceIntegrationTest.java @@ -16,6 +16,8 @@ */ package org.apache.solr.mcp.server.metadata; +import static org.junit.jupiter.api.Assertions.*; + import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.request.CollectionAdminRequest; import org.apache.solr.client.solrj.response.schema.SchemaRepresentation; @@ -26,30 +28,27 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; -import static org.junit.jupiter.api.Assertions.*; - /** - * Integration test suite for SchemaService using real Solr containers. - * Tests actual schema retrieval functionality against a live Solr instance. + * Integration test suite for SchemaService using real Solr containers. Tests actual schema + * retrieval functionality against a live Solr instance. */ @SpringBootTest @Import(TestcontainersConfiguration.class) class SchemaServiceIntegrationTest { - @Autowired - private SchemaService schemaService; + @Autowired private SchemaService schemaService; - @Autowired - private SolrClient solrClient; + @Autowired private SolrClient solrClient; private static final String TEST_COLLECTION = "schema_test_collection"; private static boolean initialized = false; @BeforeEach void setupCollection() throws Exception { - // Create a collection for testing + // Create a collection for testing if (!initialized) { - CollectionAdminRequest.Create createRequest = CollectionAdminRequest.createCollection(TEST_COLLECTION, "_default", 1, 1); + CollectionAdminRequest.Create createRequest = + CollectionAdminRequest.createCollection(TEST_COLLECTION, "_default", 1, 1); createRequest.process(solrClient); initialized = true; } @@ -64,44 +63,57 @@ void testGetSchema_ValidCollection() throws Exception { assertNotNull(schema, "Schema should not be null"); assertNotNull(schema.getFields(), "Schema fields should not be null"); assertNotNull(schema.getFieldTypes(), "Schema field types should not be null"); - + // Verify basic schema properties assertFalse(schema.getFields().isEmpty(), "Schema should have at least some fields"); - assertFalse(schema.getFieldTypes().isEmpty(), "Schema should have at least some field types"); - + assertFalse( + schema.getFieldTypes().isEmpty(), "Schema should have at least some field types"); + // Check for common default fields in Solr - boolean hasIdField = schema.getFields().stream() - .anyMatch(field -> "id".equals(field.get("name"))); + boolean hasIdField = + schema.getFields().stream().anyMatch(field -> "id".equals(field.get("name"))); assertTrue(hasIdField, "Schema should have an 'id' field"); - + // Check for common field types - boolean hasStringType = schema.getFieldTypes().stream() - .anyMatch(fieldType -> "string".equals(fieldType.getAttributes().get("name"))); + boolean hasStringType = + schema.getFieldTypes().stream() + .anyMatch( + fieldType -> + "string".equals(fieldType.getAttributes().get("name"))); assertTrue(hasStringType, "Schema should have a 'string' field type"); } @Test void testGetSchema_InvalidCollection() { // When/Then - assertThrows(Exception.class, () -> { - schemaService.getSchema("non_existent_collection_12345"); - }, "Getting schema for non-existent collection should throw exception"); + assertThrows( + Exception.class, + () -> { + schemaService.getSchema("non_existent_collection_12345"); + }, + "Getting schema for non-existent collection should throw exception"); } @Test void testGetSchema_NullCollection() { // When/Then - assertThrows(Exception.class, () -> { - schemaService.getSchema(null); - }, "Getting schema with null collection should throw exception"); + assertThrows( + Exception.class, + () -> { + schemaService.getSchema(null); + }, + "Getting schema with null collection should throw exception"); } @Test void testGetSchema_EmptyCollection() { // When/Then - assertThrows(Exception.class, () -> { - schemaService.getSchema(""); - }, "Getting schema with empty collection should throw exception"); + assertThrows( + Exception.class, + () -> { + schemaService.getSchema(""); + }, + "Getting schema with empty collection should throw exception"); } @Test @@ -113,17 +125,25 @@ void testGetSchema_ValidatesSchemaContent() throws Exception { assertNotNull(schema.getName(), "Schema should have a name"); // Check that we can access field details - schema.getFields().forEach(field -> { - assertNotNull(field.get("name"), "Field should have a name"); - assertNotNull(field.get("type"), "Field should have a type"); - // indexed and stored can be null (defaults to true in many cases) - }); + schema.getFields() + .forEach( + field -> { + assertNotNull(field.get("name"), "Field should have a name"); + assertNotNull(field.get("type"), "Field should have a type"); + // indexed and stored can be null (defaults to true in many cases) + }); // Check that we can access field type details - schema.getFieldTypes().forEach(fieldType -> { - assertNotNull(fieldType.getAttributes().get("name"), "Field type should have a name"); - assertNotNull(fieldType.getAttributes().get("class"), "Field type should have a class"); - }); + schema.getFieldTypes() + .forEach( + fieldType -> { + assertNotNull( + fieldType.getAttributes().get("name"), + "Field type should have a name"); + assertNotNull( + fieldType.getAttributes().get("class"), + "Field type should have a class"); + }); } @Test @@ -133,17 +153,20 @@ void testGetSchema_ChecksDynamicFields() throws Exception { // Then - verify dynamic fields are accessible assertNotNull(schema.getDynamicFields(), "Dynamic fields should not be null"); - + // Most Solr schemas have some dynamic fields by default assertTrue(schema.getDynamicFields().size() >= 0, "Dynamic fields should be a valid list"); - + // Check for common dynamic field patterns - boolean hasStringDynamicField = schema.getDynamicFields().stream() - .anyMatch(dynField -> { - String name = (String) dynField.get("name"); - return name != null && (name.contains("*_s") || name.contains("*_str")); - }); - + boolean hasStringDynamicField = + schema.getDynamicFields().stream() + .anyMatch( + dynField -> { + String name = (String) dynField.get("name"); + return name != null + && (name.contains("*_s") || name.contains("*_str")); + }); + assertTrue(hasStringDynamicField, "Schema should have string dynamic fields"); } @@ -154,7 +177,7 @@ void testGetSchema_ChecksCopyFields() throws Exception { // Then - verify copy fields are accessible assertNotNull(schema.getCopyFields(), "Copy fields should not be null"); - + // Copy fields list can be empty, that's valid assertTrue(schema.getCopyFields().size() >= 0, "Copy fields should be a valid list"); } @@ -171,4 +194,4 @@ void testGetSchema_ReturnsUniqueKey() throws Exception { assertNotNull(schema.getUniqueKey(), "Unique key should be accessible"); } } -} \ No newline at end of file +} diff --git a/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceTest.java b/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceTest.java index bcee158..803e94e 100644 --- a/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceTest.java +++ b/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceTest.java @@ -16,6 +16,12 @@ */ package org.apache.solr.mcp.server.metadata; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.io.IOException; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.request.schema.SchemaRequest; @@ -27,28 +33,18 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - /** - * Comprehensive test suite for the SchemaService class. - * Tests schema retrieval functionality with various scenarios including success and error cases. + * Comprehensive test suite for the SchemaService class. Tests schema retrieval functionality with + * various scenarios including success and error cases. */ @ExtendWith(MockitoExtension.class) class SchemaServiceTest { - @Mock - private SolrClient solrClient; + @Mock private SolrClient solrClient; - @Mock - private SchemaResponse schemaResponse; + @Mock private SchemaResponse schemaResponse; - @Mock - private SchemaRepresentation schemaRepresentation; + @Mock private SchemaRepresentation schemaRepresentation; private SchemaService schemaService; @@ -61,7 +57,7 @@ void setUp() { void testSchemaService_InstantiatesCorrectly() { // Given/When SchemaService service = new SchemaService(solrClient); - + // Then assertNotNull(service, "SchemaService should be instantiated correctly"); } @@ -70,63 +66,74 @@ void testSchemaService_InstantiatesCorrectly() { void testGetSchema_CollectionNotFound() throws Exception { // Given final String nonExistentCollection = "non_existent_collection"; - + // When SolrClient throws an exception for non-existent collection when(solrClient.request(any(SchemaRequest.class), eq(nonExistentCollection))) - .thenThrow(new SolrServerException("Collection not found: " + nonExistentCollection)); + .thenThrow( + new SolrServerException("Collection not found: " + nonExistentCollection)); // Then - assertThrows(Exception.class, () -> { - schemaService.getSchema(nonExistentCollection); - }); + assertThrows( + Exception.class, + () -> { + schemaService.getSchema(nonExistentCollection); + }); } @Test void testGetSchema_SolrServerException() throws Exception { // Given final String collectionName = "test_collection"; - + // When SolrClient throws a SolrServerException when(solrClient.request(any(SchemaRequest.class), eq(collectionName))) .thenThrow(new SolrServerException("Solr server error")); // Then - assertThrows(Exception.class, () -> { - schemaService.getSchema(collectionName); - }); + assertThrows( + Exception.class, + () -> { + schemaService.getSchema(collectionName); + }); } @Test void testGetSchema_IOException() throws Exception { // Given final String collectionName = "test_collection"; - + // When SolrClient throws an IOException when(solrClient.request(any(SchemaRequest.class), eq(collectionName))) .thenThrow(new IOException("Network connection error")); // Then - assertThrows(Exception.class, () -> { - schemaService.getSchema(collectionName); - }); + assertThrows( + Exception.class, + () -> { + schemaService.getSchema(collectionName); + }); } @Test void testGetSchema_WithNullCollection() { // Given a null collection name // Then should throw an exception (NullPointerException or IllegalArgumentException) - assertThrows(Exception.class, () -> { - schemaService.getSchema(null); - }); + assertThrows( + Exception.class, + () -> { + schemaService.getSchema(null); + }); } @Test void testGetSchema_WithEmptyCollection() { // Given an empty collection name // Then should throw an exception - assertThrows(Exception.class, () -> { - schemaService.getSchema(""); - }); + assertThrows( + Exception.class, + () -> { + schemaService.getSchema(""); + }); } @Test @@ -139,8 +146,9 @@ void testConstructor() { @Test void testConstructor_WithNullClient() { // Test constructor with null client - assertDoesNotThrow(() -> { - new SchemaService(null); - }); + assertDoesNotThrow( + () -> { + new SchemaService(null); + }); } -} \ No newline at end of file +} 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 263f630..304bc52 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 @@ -16,6 +16,15 @@ */ package org.apache.solr.mcp.server.search; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.ArrayList; +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; @@ -29,24 +38,12 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class SearchServiceDirectTest { - @Mock - private SolrClient solrClient; + @Mock private SolrClient solrClient; - @Mock - private QueryResponse queryResponse; + @Mock private QueryResponse queryResponse; private SearchService searchService; @@ -147,7 +144,8 @@ void testSearchWithEmptyResults() throws SolrServerException, IOException { when(solrClient.query(eq("books"), any(SolrQuery.class))).thenReturn(queryResponse); // Test - SearchResponse result = searchService.search("books", "nonexistent_query", null, null, null, null, null); + SearchResponse result = + searchService.search("books", "nonexistent_query", null, null, null, null, null); // Verify assertNotNull(result); @@ -180,7 +178,8 @@ void testSearchWithEmptyFacets() throws SolrServerException, IOException { when(solrClient.query(eq("books"), any(SolrQuery.class))).thenReturn(queryResponse); // Test with facet fields requested but none returned - SearchResponse result = searchService.search("books", null, null, List.of("genre_s"), null, null, null); + SearchResponse result = + searchService.search("books", null, null, List.of("genre_s"), null, null, null); // Verify assertNotNull(result); @@ -215,7 +214,8 @@ void testSearchWithEmptyFacetValues() throws SolrServerException, IOException { when(solrClient.query(eq("books"), any(SolrQuery.class))).thenReturn(queryResponse); // Test - SearchResponse result = searchService.search("books", null, null, List.of("genre_s"), null, null, null); + SearchResponse result = + searchService.search("books", null, null, List.of("genre_s"), null, null, null); // Verify assertNotNull(result); @@ -229,12 +229,14 @@ void testSearchWithSolrError() { // Setup mock to throw exception try { when(solrClient.query(eq("books"), any(SolrQuery.class))) - .thenThrow(new SolrServerException("Simulated Solr server error")); + .thenThrow(new SolrServerException("Simulated Solr server error")); // Test - assertThrows(SolrServerException.class, () -> { - searchService.search("books", null, null, null, null, null, null); - }); + assertThrows( + SolrServerException.class, + () -> { + searchService.search("books", null, null, null, null, null, null); + }); } catch (Exception e) { fail("Test setup failed: " + e.getMessage()); } @@ -270,12 +272,11 @@ void testSearchWithAllParameters() throws SolrServerException, IOException { // Test with all parameters List filterQueries = List.of("price:[10 TO 15]"); List facetFields2 = List.of("genre_s", "author"); - List> sortClauses = List.of( - Map.of("item", "price", "order", "desc") - ); + List> sortClauses = List.of(Map.of("item", "price", "order", "desc")); - SearchResponse result = searchService.search( - "books", "mystery", filterQueries, facetFields2, sortClauses, 5, 10); + SearchResponse result = + searchService.search( + "books", "mystery", filterQueries, facetFields2, sortClauses, 5, 10); // Verify assertNotNull(result); 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 ea1f238..fc1ed07 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 @@ -16,6 +16,17 @@ */ package org.apache.solr.mcp.server.search; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +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; @@ -32,21 +43,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.OptionalDouble; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Combined tests for SearchService: integration + unit (mocked SolrClient) in one class. - */ +/** Combined tests for SearchService: integration + unit (mocked SolrClient) in one class. */ @SpringBootTest @Import(TestcontainersConfiguration.class) class SearchServiceTest { @@ -54,23 +51,21 @@ class SearchServiceTest { // ===== Integration test context ===== private static final String COLLECTION_NAME = "search_test_" + System.currentTimeMillis(); - @Autowired - private SearchService searchService; - @Autowired - private IndexingService indexingService; - @Autowired - private SolrClient solrClient; + @Autowired private SearchService searchService; + @Autowired private IndexingService indexingService; + @Autowired private SolrClient solrClient; private static boolean initialized = false; @BeforeEach void setUp() throws Exception { if (!initialized) { - CollectionAdminRequest.Create createRequest = CollectionAdminRequest.createCollection( - COLLECTION_NAME, "_default", 1, 1); + CollectionAdminRequest.Create createRequest = + CollectionAdminRequest.createCollection(COLLECTION_NAME, "_default", 1, 1); createRequest.process(solrClient); - String sampleData = """ + String sampleData = + """ [ { "id": "book001", @@ -185,7 +180,8 @@ void setUp() throws Exception { @Test void testBasicSearch() throws SolrServerException, IOException { - SearchResponse result = searchService.search(COLLECTION_NAME, null, null, null, null, null, null); + SearchResponse result = + searchService.search(COLLECTION_NAME, null, null, null, null, null, null); assertNotNull(result); List> documents = result.documents(); assertFalse(documents.isEmpty()); @@ -194,7 +190,9 @@ void testBasicSearch() throws SolrServerException, IOException { @Test void testSearchWithQuery() throws SolrServerException, IOException { - SearchResponse result = searchService.search(COLLECTION_NAME, "name:\"Game of Thrones\"", null, null, null, null, null); + SearchResponse result = + searchService.search( + COLLECTION_NAME, "name:\"Game of Thrones\"", null, null, null, null, null); assertNotNull(result); List> documents = result.documents(); assertEquals(1, documents.size()); @@ -204,8 +202,15 @@ void testSearchWithQuery() throws SolrServerException, IOException { @Test void testSearchReturnsAuthor() throws Exception { - SearchResponse result = searchService.search( - COLLECTION_NAME, "author_ss:\"George R.R. Martin\"", null, null, null, null, null); + SearchResponse result = + searchService.search( + COLLECTION_NAME, + "author_ss:\"George R.R. Martin\"", + null, + null, + null, + null, + null); assertNotNull(result); List> documents = result.documents(); assertEquals(3, documents.size()); @@ -215,8 +220,9 @@ void testSearchReturnsAuthor() throws Exception { @Test void testSearchWithFacets() throws Exception { - SearchResponse result = searchService.search( - COLLECTION_NAME, null, null, List.of("genre_s"), null, null, null); + SearchResponse result = + searchService.search( + COLLECTION_NAME, null, null, List.of("genre_s"), null, null, null); assertNotNull(result); Map> facets = result.facets(); assertNotNull(facets); @@ -225,23 +231,24 @@ void testSearchWithFacets() throws Exception { @Test void testSearchWithPrice() throws Exception { - SearchResponse result = searchService.search( - COLLECTION_NAME, null, null, null, null, null, null); + SearchResponse result = + searchService.search(COLLECTION_NAME, null, null, null, null, null, null); assertNotNull(result); List> documents = result.documents(); assertFalse(documents.isEmpty()); Map book = documents.getFirst(); - double currentPrice = ((List) book.get("price")).isEmpty() ? 0.0 : ((Number) ((List) book.get("price")).getFirst()).doubleValue(); + double currentPrice = + ((List) book.get("price")).isEmpty() + ? 0.0 + : ((Number) ((List) book.get("price")).getFirst()).doubleValue(); assertTrue(currentPrice > 0); } @Test void testSortByPriceAscending() throws Exception { - List> sortClauses = List.of( - Map.of("item", "price", "order", "asc") - ); - SearchResponse result = searchService.search( - COLLECTION_NAME, null, null, null, sortClauses, null, null); + List> sortClauses = List.of(Map.of("item", "price", "order", "asc")); + SearchResponse result = + searchService.search(COLLECTION_NAME, null, null, null, sortClauses, null, null); assertNotNull(result); List> documents = result.documents(); assertFalse(documents.isEmpty()); @@ -250,18 +257,18 @@ void testSortByPriceAscending() throws Exception { OptionalDouble priceOpt = extractPrice(book); if (priceOpt.isEmpty()) continue; double currentPrice = priceOpt.getAsDouble(); - assertTrue(currentPrice >= previousPrice, "Books should be sorted by price in ascending order"); + assertTrue( + currentPrice >= previousPrice, + "Books should be sorted by price in ascending order"); previousPrice = currentPrice; } } @Test void testSortByPriceDescending() throws Exception { - List> sortClauses = List.of( - Map.of("item", "price", "order", "desc") - ); - SearchResponse result = searchService.search( - COLLECTION_NAME, null, null, null, sortClauses, null, null); + List> sortClauses = List.of(Map.of("item", "price", "order", "desc")); + SearchResponse result = + searchService.search(COLLECTION_NAME, null, null, null, sortClauses, null, null); assertNotNull(result); List> documents = result.documents(); assertFalse(documents.isEmpty()); @@ -270,26 +277,30 @@ void testSortByPriceDescending() throws Exception { OptionalDouble priceOpt = extractPrice(book); if (priceOpt.isEmpty()) continue; double currentPrice = priceOpt.getAsDouble(); - assertTrue(currentPrice <= previousPrice, "Books should be sorted by price in descending order"); + assertTrue( + currentPrice <= previousPrice, + "Books should be sorted by price in descending order"); previousPrice = currentPrice; } } @Test void testSortBySequence() throws Exception { - List> sortClauses = List.of( - Map.of("item", "sequence_i", "order", "asc") - ); + List> sortClauses = + List.of(Map.of("item", "sequence_i", "order", "asc")); List filterQueries = List.of("series_s:\"A Song of Ice and Fire\""); - SearchResponse result = searchService.search( - COLLECTION_NAME, null, filterQueries, null, sortClauses, null, null); + SearchResponse result = + searchService.search( + COLLECTION_NAME, null, filterQueries, null, sortClauses, null, null); assertNotNull(result); List> documents = result.documents(); assertFalse(documents.isEmpty()); int previousSequence = 0; for (Map book : documents) { int currentSequence = ((Number) book.get("sequence_i")).intValue(); - assertTrue(currentSequence >= previousSequence, "Books should be sorted by sequence_i in ascending order"); + assertTrue( + currentSequence >= previousSequence, + "Books should be sorted by sequence_i in ascending order"); previousSequence = currentSequence; } } @@ -297,8 +308,8 @@ void testSortBySequence() throws Exception { @Test void testFilterByGenre() throws Exception { List filterQueries = List.of("genre_s:fantasy"); - SearchResponse result = searchService.search( - COLLECTION_NAME, null, filterQueries, null, null, null, null); + SearchResponse result = + searchService.search(COLLECTION_NAME, null, filterQueries, null, null, null, null); assertNotNull(result); List> documents = result.documents(); assertFalse(documents.isEmpty()); @@ -311,8 +322,8 @@ void testFilterByGenre() throws Exception { @Test void testFilterByPriceRange() throws Exception { List filterQueries = List.of("price:[6.0 TO 7.0]"); - SearchResponse result = searchService.search( - COLLECTION_NAME, null, filterQueries, null, null, null, null); + SearchResponse result = + searchService.search(COLLECTION_NAME, null, filterQueries, null, null, null, null); assertNotNull(result); List> documents = result.documents(); assertFalse(documents.isEmpty()); @@ -321,18 +332,19 @@ void testFilterByPriceRange() throws Exception { OptionalDouble priceOpt = extractPrice(book); if (priceOpt.isEmpty()) continue; double price = priceOpt.getAsDouble(); - assertTrue(price >= 6.0 && price <= 7.0, "All books should have price between 6.0 and 7.0"); + assertTrue( + price >= 6.0 && price <= 7.0, + "All books should have price between 6.0 and 7.0"); } } @Test void testCombinedSortingAndFiltering() throws Exception { - List> sortClauses = List.of( - Map.of("item", "price", "order", "desc") - ); + List> sortClauses = List.of(Map.of("item", "price", "order", "desc")); List filterQueries = List.of("genre_s:fantasy"); - SearchResponse result = searchService.search( - COLLECTION_NAME, null, filterQueries, null, sortClauses, null, null); + SearchResponse result = + searchService.search( + COLLECTION_NAME, null, filterQueries, null, sortClauses, null, null); assertNotNull(result); List> documents = result.documents(); assertFalse(documents.isEmpty()); @@ -355,26 +367,28 @@ void testCombinedSortingAndFiltering() throws Exception { } else { continue; } - assertTrue(currentPrice <= previousPrice, "Books should be sorted by price in descending order"); + assertTrue( + currentPrice <= previousPrice, + "Books should be sorted by price in descending order"); previousPrice = currentPrice; } } @Test void testPagination() throws Exception { - SearchResponse allResults = searchService.search( - COLLECTION_NAME, null, null, null, null, null, null); + SearchResponse allResults = + searchService.search(COLLECTION_NAME, null, null, null, null, null, null); assertNotNull(allResults); long totalDocuments = allResults.numFound(); assertTrue(totalDocuments > 0, "Should have at least some documents"); - SearchResponse firstPage = searchService.search( - COLLECTION_NAME, null, null, null, null, 0, 2); + SearchResponse firstPage = + searchService.search(COLLECTION_NAME, null, null, null, null, 0, 2); assertNotNull(firstPage); assertEquals(0, firstPage.start(), "Start offset should be 0"); assertEquals(totalDocuments, firstPage.numFound(), "Total count should match"); assertEquals(2, firstPage.documents().size(), "Should return exactly 2 documents"); - SearchResponse secondPage = searchService.search( - COLLECTION_NAME, null, null, null, null, 2, 2); + SearchResponse secondPage = + searchService.search(COLLECTION_NAME, null, null, null, null, 2, 2); assertNotNull(secondPage); assertEquals(2, secondPage.start(), "Start offset should be 2"); assertEquals(totalDocuments, secondPage.numFound(), "Total count should match"); @@ -382,26 +396,30 @@ void testPagination() throws Exception { List firstPageIds = getDocumentIds(firstPage.documents()); List secondPageIds = getDocumentIds(secondPage.documents()); for (String id : firstPageIds) { - assertFalse(secondPageIds.contains(id), "Second page should not contain documents from first page"); + assertFalse( + secondPageIds.contains(id), + "Second page should not contain documents from first page"); } } @Test void testSpecialCharactersInQuery() throws Exception { - String specialJson = """ - [ - { - "id": "special001", - "title": "Book with special characters: & + - ! ( ) { } [ ] ^ \\" ~ * ? : \\\\ /", - "author_ss": ["Special Author (with parentheses)"], - "description": "This is a test document with special characters: & + - ! ( ) { } [ ] ^ \\" ~ * ? : \\\\ /" - } - ] - """; + String specialJson = + """ +[ + { + "id": "special001", + "title": "Book with special characters: & + - ! ( ) { } [ ] ^ \\" ~ * ? : \\\\ /", + "author_ss": ["Special Author (with parentheses)"], + "description": "This is a test document with special characters: & + - ! ( ) { } [ ] ^ \\" ~ * ? : \\\\ /" + } +] +"""; indexingService.indexJsonDocuments(COLLECTION_NAME, specialJson); solrClient.commit(COLLECTION_NAME); String query = "id:special001"; - SearchResponse result = searchService.search(COLLECTION_NAME, query, null, null, null, null, null); + SearchResponse result = + searchService.search(COLLECTION_NAME, query, null, null, null, null, null); assertNotNull(result); assertEquals(1, result.numFound(), "Should find exactly one document"); query = "author_ss:\"Special Author \\(" + "with parentheses\\)\""; // escape parentheses @@ -429,13 +447,16 @@ void unit_search_WithNullQuery_ShouldDefaultToMatchAll() throws Exception { SolrDocumentList mockDocuments = createMockDocumentList(); when(mockResponse.getResults()).thenReturn(mockDocuments); when(mockResponse.getFacetFields()).thenReturn(null); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenAnswer(invocation -> { - SolrQuery q = invocation.getArgument(1); - assertEquals("*:*", q.getQuery()); - return mockResponse; - }); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenAnswer( + invocation -> { + SolrQuery q = invocation.getArgument(1); + assertEquals("*:*", q.getQuery()); + return mockResponse; + }); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", null, null, null, null, null, null); + SearchResponse result = + localService.search("test_collection", null, null, null, null, null, null); assertNotNull(result); } @@ -447,13 +468,16 @@ void unit_search_WithCustomQuery_ShouldUseProvidedQuery() throws Exception { SolrDocumentList mockDocuments = createMockDocumentList(); when(mockResponse.getResults()).thenReturn(mockDocuments); when(mockResponse.getFacetFields()).thenReturn(null); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenAnswer(invocation -> { - SolrQuery q = invocation.getArgument(1); - assertEquals(customQuery, q.getQuery()); - return mockResponse; - }); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenAnswer( + invocation -> { + SolrQuery q = invocation.getArgument(1); + assertEquals(customQuery, q.getQuery()); + return mockResponse; + }); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", customQuery, null, null, null, null, null); + SearchResponse result = + localService.search("test_collection", customQuery, null, null, null, null, null); assertNotNull(result); } @@ -465,13 +489,16 @@ void unit_search_WithFilterQueries_ShouldApplyFilters() throws Exception { SolrDocumentList mockDocuments = createMockDocumentList(); when(mockResponse.getResults()).thenReturn(mockDocuments); when(mockResponse.getFacetFields()).thenReturn(null); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenAnswer(invocation -> { - SolrQuery q = invocation.getArgument(1); - assertArrayEquals(filterQueries.toArray(), q.getFilterQueries()); - return mockResponse; - }); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenAnswer( + invocation -> { + SolrQuery q = invocation.getArgument(1); + assertArrayEquals(filterQueries.toArray(), q.getFilterQueries()); + return mockResponse; + }); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", null, filterQueries, null, null, null, null); + SearchResponse result = + localService.search("test_collection", null, filterQueries, null, null, null, null); assertNotNull(result); } @@ -483,9 +510,11 @@ void unit_search_WithFacetFields_ShouldEnableFaceting() throws Exception { SolrDocumentList mockDocuments = createMockDocumentList(); when(mockResponse.getResults()).thenReturn(mockDocuments); when(mockResponse.getFacetFields()).thenReturn(createMockFacetFields()); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenAnswer(invocation -> mockResponse); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenAnswer(invocation -> mockResponse); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", null, null, facetFields, null, null, null); + SearchResponse result = + localService.search("test_collection", null, null, facetFields, null, null, null); assertNotNull(result); assertNotNull(result.facets()); } @@ -494,16 +523,18 @@ void unit_search_WithFacetFields_ShouldEnableFaceting() throws Exception { void unit_search_WithSortClauses_ShouldApplySorting() throws Exception { SolrClient mockClient = mock(SolrClient.class); QueryResponse mockResponse = mock(QueryResponse.class); - List> sortClauses = List.of( - Map.of("item", "price", "order", "asc"), - Map.of("item", "name", "order", "desc") - ); + List> sortClauses = + List.of( + Map.of("item", "price", "order", "asc"), + Map.of("item", "name", "order", "desc")); SolrDocumentList mockDocuments = createMockDocumentList(); when(mockResponse.getResults()).thenReturn(mockDocuments); when(mockResponse.getFacetFields()).thenReturn(null); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenAnswer(invocation -> mockResponse); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenAnswer(invocation -> mockResponse); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", null, null, null, sortClauses, null, null); + SearchResponse result = + localService.search("test_collection", null, null, null, sortClauses, null, null); assertNotNull(result); } @@ -516,14 +547,17 @@ void unit_search_WithPagination_ShouldApplyStartAndRows() throws Exception { SolrDocumentList mockDocuments = createMockDocumentList(); when(mockResponse.getResults()).thenReturn(mockDocuments); when(mockResponse.getFacetFields()).thenReturn(null); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenAnswer(invocation -> { - SolrQuery q = invocation.getArgument(1); - assertEquals(start, q.getStart()); - assertEquals(rows, q.getRows()); - return mockResponse; - }); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenAnswer( + invocation -> { + SolrQuery q = invocation.getArgument(1); + assertEquals(start, q.getStart()); + assertEquals(rows, q.getRows()); + return mockResponse; + }); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", null, null, null, null, start, rows); + SearchResponse result = + localService.search("test_collection", null, null, null, null, start, rows); assertNotNull(result); } @@ -540,17 +574,27 @@ void unit_search_WithAllParameters_ShouldCombineAllOptions() throws Exception { SolrDocumentList mockDocuments = createMockDocumentList(); when(mockResponse.getResults()).thenReturn(mockDocuments); when(mockResponse.getFacetFields()).thenReturn(createMockFacetFields()); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenAnswer(invocation -> { - SolrQuery captured = invocation.getArgument(1); - assertEquals(query, captured.getQuery()); - assertArrayEquals(filterQueries.toArray(), captured.getFilterQueries()); - assertNotNull(captured.getFacetFields()); - assertEquals(start, captured.getStart()); - assertEquals(rows, captured.getRows()); - return mockResponse; - }); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenAnswer( + invocation -> { + SolrQuery captured = invocation.getArgument(1); + assertEquals(query, captured.getQuery()); + assertArrayEquals(filterQueries.toArray(), captured.getFilterQueries()); + assertNotNull(captured.getFacetFields()); + assertEquals(start, captured.getStart()); + assertEquals(rows, captured.getRows()); + return mockResponse; + }); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", query, filterQueries, facetFields, sortClauses, start, rows); + SearchResponse result = + localService.search( + "test_collection", + query, + filterQueries, + facetFields, + sortClauses, + start, + rows); assertNotNull(result); } @@ -560,8 +604,9 @@ void unit_search_WhenSolrThrowsException_ShouldPropagateException() throws Excep when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) .thenThrow(new SolrServerException("Connection error")); SearchService localService = new SearchService(mockClient); - assertThrows(SolrServerException.class, () -> - localService.search("test_collection", null, null, null, null, null, null)); + assertThrows( + SolrServerException.class, + () -> localService.search("test_collection", null, null, null, null, null, null)); } @Test @@ -570,8 +615,9 @@ void unit_search_WhenIOException_ShouldPropagateException() throws Exception { when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) .thenThrow(new IOException("Network error")); SearchService localService = new SearchService(mockClient); - assertThrows(IOException.class, () -> - localService.search("test_collection", null, null, null, null, null, null)); + assertThrows( + IOException.class, + () -> localService.search("test_collection", null, null, null, null, null, null)); } @Test @@ -583,9 +629,12 @@ void unit_search_WithEmptyResults_ShouldReturnEmptyDocumentList() throws Excepti emptyDocuments.setStart(0); when(mockResponse.getResults()).thenReturn(emptyDocuments); when(mockResponse.getFacetFields()).thenReturn(null); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenReturn(mockResponse); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenReturn(mockResponse); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", "nonexistent:value", null, null, null, null, null); + SearchResponse result = + localService.search( + "test_collection", "nonexistent:value", null, null, null, null, null); assertNotNull(result); assertEquals(0, result.numFound()); assertTrue(result.documents().isEmpty()); @@ -598,13 +647,16 @@ void unit_search_WithNullFilterQueries_ShouldNotApplyFilters() throws Exception SolrDocumentList mockDocuments = createMockDocumentList(); when(mockResponse.getResults()).thenReturn(mockDocuments); when(mockResponse.getFacetFields()).thenReturn(null); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenAnswer(invocation -> { - SolrQuery q = invocation.getArgument(1); - assertNull(q.getFilterQueries()); - return mockResponse; - }); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenAnswer( + invocation -> { + SolrQuery q = invocation.getArgument(1); + assertNull(q.getFilterQueries()); + return mockResponse; + }); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", null, null, null, null, null, null); + SearchResponse result = + localService.search("test_collection", null, null, null, null, null, null); assertNotNull(result); } @@ -615,13 +667,16 @@ void unit_search_WithEmptyFacetFields_ShouldNotEnableFaceting() throws Exception SolrDocumentList mockDocuments = createMockDocumentList(); when(mockResponse.getResults()).thenReturn(mockDocuments); when(mockResponse.getFacetFields()).thenReturn(null); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenAnswer(invocation -> { - SolrQuery q = invocation.getArgument(1); - assertNull(q.getFacetFields()); - return mockResponse; - }); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenAnswer( + invocation -> { + SolrQuery q = invocation.getArgument(1); + assertNull(q.getFacetFields()); + return mockResponse; + }); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", null, null, List.of(), null, null, null); + SearchResponse result = + localService.search("test_collection", null, null, List.of(), null, null, null); assertNotNull(result); } @@ -632,9 +687,12 @@ void unit_searchResponse_ShouldContainAllFields() throws Exception { SolrDocumentList mockDocuments = createMockDocumentListWithData(); when(mockResponse.getResults()).thenReturn(mockDocuments); when(mockResponse.getFacetFields()).thenReturn(createMockFacetFields()); - when(mockClient.query(eq("test_collection"), any(SolrQuery.class))).thenReturn(mockResponse); + when(mockClient.query(eq("test_collection"), any(SolrQuery.class))) + .thenReturn(mockResponse); SearchService localService = new SearchService(mockClient); - SearchResponse result = localService.search("test_collection", null, null, List.of("genre_s"), null, null, null); + SearchResponse result = + localService.search( + "test_collection", null, null, List.of("genre_s"), null, null, null); assertNotNull(result); assertEquals(2, result.numFound()); assertEquals(0, result.start());