This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit 0bc9c861ae34501a865a0269ad49179c13fb5740 Author: Robert Lazarski <[email protected]> AuthorDate: Wed May 13 11:44:21 2026 -1000 Add autoconfiguration tests for embedded Tomcat support 8 new tests using Mockito to verify the axis2.repo property logic in both Axis2ServletAutoConfiguration and Axis2RepositoryAutoConfiguration: Axis2ServletAutoConfiguration (6 tests): - repoProperty_overridesGetRealPath: axis2.repo takes precedence over ServletContext.getRealPath(); both axis2.repository.path and axis2.xml.path init-params are set correctly - emptyRepoProperty_usesGetRealPath: default behavior unchanged when axis2.repo is empty - repoProperty_failsFastWhenAxis2XmlMissing: throws IllegalStateException when axis2.repo is set but conf/axis2.xml is absent - noAxis2XmlPath_whenFileAbsentAndRepoNotSet: getRealPath case does not fail-fast (backward compatible) - servletMappedToConfiguredPath: custom services-path is applied - servletMappingConflict_throwsException: validates error on servlet mapping conflict Axis2RepositoryAutoConfiguration (2 tests): - repoProperty_usedForAxis2XmlStaging: axis2.repo overrides getRealPath for staging - nullRealPath_andEmptyRepo_logsWarning: graceful fallback when both are unavailable 19 tests total (11 existing + 8 new), all pass. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --- .../boot/Axis2ServletAutoConfigurationTest.java | 246 +++++++++++++++++++++ src/site/xdoc/docs/spring-boot-starter.xml | 133 +++++++++-- 2 files changed, 360 insertions(+), 19 deletions(-) diff --git a/modules/spring-boot-starter/src/test/java/org/apache/axis2/spring/boot/Axis2ServletAutoConfigurationTest.java b/modules/spring-boot-starter/src/test/java/org/apache/axis2/spring/boot/Axis2ServletAutoConfigurationTest.java new file mode 100644 index 0000000000..ffb47165c1 --- /dev/null +++ b/modules/spring-boot-starter/src/test/java/org/apache/axis2/spring/boot/Axis2ServletAutoConfigurationTest.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.axis2.spring.boot; + +import org.apache.axis2.deployment.WarBasedAxisConfigurator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletRegistration; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Tests for {@link Axis2ServletAutoConfiguration} and + * {@link Axis2RepositoryAutoConfiguration} autoconfiguration logic. + * + * <p>Verifies that the axis2.repo property correctly overrides + * ServletContext.getRealPath() and that the axis2.xml.path init-parameter + * is set when the repo contains conf/axis2.xml. + */ +class Axis2ServletAutoConfigurationTest { + + // ═══════════════════════════════════════════════════════════════════════ + // Axis2ServletAutoConfiguration — repository and axis2.xml path + // ═══════════════════════════════════════════════════════════════════════ + + @Test + void repoProperty_overridesGetRealPath(@TempDir Path tempDir) throws Exception { + // Create the expected directory structure + Path confDir = tempDir.resolve("conf"); + Files.createDirectories(confDir); + Files.writeString(confDir.resolve("axis2.xml"), "<axisconfig/>"); + + Axis2Properties properties = new Axis2Properties(); + properties.setRepo(tempDir.toString()); + + // Mock ServletContext that returns null for getRealPath (embedded mode) + ServletContext servletContext = mock(ServletContext.class); + when(servletContext.getRealPath("/WEB-INF")).thenReturn(null); + + ServletRegistration.Dynamic axisServlet = mock(ServletRegistration.Dynamic.class); + when(servletContext.addServlet(eq("AxisServlet"), any(org.apache.axis2.transport.http.AxisServlet.class))) + .thenReturn(axisServlet); + when(axisServlet.addMapping(anyString())).thenReturn(Collections.emptySet()); + + // Execute + Axis2ServletAutoConfiguration config = new Axis2ServletAutoConfiguration(); + config.axis2ServletInitializer(properties).onStartup(servletContext); + + // Verify repository path was set from axis2.repo, not getRealPath + ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class); + verify(axisServlet, atLeast(2)).setInitParameter(nameCaptor.capture(), valueCaptor.capture()); + + Map<String, String> params = new HashMap<>(); + for (int i = 0; i < nameCaptor.getAllValues().size(); i++) { + params.put(nameCaptor.getAllValues().get(i), valueCaptor.getAllValues().get(i)); + } + + assertEquals(tempDir.toString(), + params.get(WarBasedAxisConfigurator.PARAM_AXIS2_REPOSITORY_PATH), + "repository path should come from axis2.repo property"); + assertNotNull(params.get(WarBasedAxisConfigurator.PARAM_AXIS2_XML_PATH), + "axis2.xml.path should be set when conf/axis2.xml exists"); + assertTrue(params.get(WarBasedAxisConfigurator.PARAM_AXIS2_XML_PATH) + .endsWith("axis2.xml"), + "axis2.xml.path should point to conf/axis2.xml"); + } + + @Test + void emptyRepoProperty_usesGetRealPath(@TempDir Path tempDir) throws Exception { + Axis2Properties properties = new Axis2Properties(); + // repo is empty — should fall back to getRealPath + + ServletContext servletContext = mock(ServletContext.class); + when(servletContext.getRealPath("/WEB-INF")).thenReturn(tempDir.toString()); + + ServletRegistration.Dynamic axisServlet = mock(ServletRegistration.Dynamic.class); + when(servletContext.addServlet(eq("AxisServlet"), any(org.apache.axis2.transport.http.AxisServlet.class))) + .thenReturn(axisServlet); + when(axisServlet.addMapping(anyString())).thenReturn(Collections.emptySet()); + + Axis2ServletAutoConfiguration config = new Axis2ServletAutoConfiguration(); + config.axis2ServletInitializer(properties).onStartup(servletContext); + + // Verify getRealPath result was used + verify(axisServlet).setInitParameter( + eq(WarBasedAxisConfigurator.PARAM_AXIS2_REPOSITORY_PATH), + eq(tempDir.toString())); + } + + @Test + void repoProperty_failsFastWhenAxis2XmlMissing(@TempDir Path tempDir) { + // Create repo dir WITHOUT conf/axis2.xml + Axis2Properties properties = new Axis2Properties(); + properties.setRepo(tempDir.toString()); + + ServletContext servletContext = mock(ServletContext.class); + when(servletContext.getRealPath("/WEB-INF")).thenReturn(null); + + ServletRegistration.Dynamic axisServlet = mock(ServletRegistration.Dynamic.class); + when(servletContext.addServlet(eq("AxisServlet"), any(org.apache.axis2.transport.http.AxisServlet.class))) + .thenReturn(axisServlet); + when(axisServlet.addMapping(anyString())).thenReturn(Collections.emptySet()); + + Axis2ServletAutoConfiguration config = new Axis2ServletAutoConfiguration(); + + // Should throw because axis2.repo is set but conf/axis2.xml is missing + assertThrows(IllegalStateException.class, + () -> config.axis2ServletInitializer(properties).onStartup(servletContext), + "Should fail fast when axis2.repo is set but axis2.xml is missing"); + } + + @Test + void repoProperty_noAxis2XmlPath_whenFileAbsentAndRepoNotSet(@TempDir Path tempDir) throws Exception { + // getRealPath returns a valid path but conf/axis2.xml doesn't exist + // AND axis2.repo is NOT set — this is fine, WarBasedAxisConfigurator + // will load from classpath as it always has + Axis2Properties properties = new Axis2Properties(); + + ServletContext servletContext = mock(ServletContext.class); + when(servletContext.getRealPath("/WEB-INF")).thenReturn(tempDir.toString()); + + ServletRegistration.Dynamic axisServlet = mock(ServletRegistration.Dynamic.class); + when(servletContext.addServlet(eq("AxisServlet"), any(org.apache.axis2.transport.http.AxisServlet.class))) + .thenReturn(axisServlet); + when(axisServlet.addMapping(anyString())).thenReturn(Collections.emptySet()); + + Axis2ServletAutoConfiguration config = new Axis2ServletAutoConfiguration(); + config.axis2ServletInitializer(properties).onStartup(servletContext); + + // Should set repository path but NOT axis2.xml.path (file doesn't exist) + verify(axisServlet).setInitParameter( + eq(WarBasedAxisConfigurator.PARAM_AXIS2_REPOSITORY_PATH), + eq(tempDir.toString())); + verify(axisServlet, never()).setInitParameter( + eq(WarBasedAxisConfigurator.PARAM_AXIS2_XML_PATH), anyString()); + } + + @Test + void servletMappedToConfiguredPath() throws Exception { + Axis2Properties properties = new Axis2Properties(); + properties.setServicesPath("/api"); + + ServletContext servletContext = mock(ServletContext.class); + when(servletContext.getRealPath("/WEB-INF")).thenReturn("/tmp/fake"); + + ServletRegistration.Dynamic axisServlet = mock(ServletRegistration.Dynamic.class); + when(servletContext.addServlet(eq("AxisServlet"), any(org.apache.axis2.transport.http.AxisServlet.class))) + .thenReturn(axisServlet); + when(axisServlet.addMapping(anyString())).thenReturn(Collections.emptySet()); + + Axis2ServletAutoConfiguration config = new Axis2ServletAutoConfiguration(); + config.axis2ServletInitializer(properties).onStartup(servletContext); + + verify(axisServlet).addMapping("/api/*"); + } + + @Test + void servletMappingConflict_throwsException() { + Axis2Properties properties = new Axis2Properties(); + + ServletContext servletContext = mock(ServletContext.class); + when(servletContext.getRealPath("/WEB-INF")).thenReturn("/tmp/fake"); + + ServletRegistration.Dynamic axisServlet = mock(ServletRegistration.Dynamic.class); + when(servletContext.addServlet(eq("AxisServlet"), any(org.apache.axis2.transport.http.AxisServlet.class))) + .thenReturn(axisServlet); + when(axisServlet.addMapping(anyString())) + .thenReturn(Collections.singleton("/services/*")); // conflict! + + Axis2ServletAutoConfiguration config = new Axis2ServletAutoConfiguration(); + + assertThrows(IllegalStateException.class, + () -> config.axis2ServletInitializer(properties).onStartup(servletContext), + "Should throw when servlet mapping conflicts"); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Axis2RepositoryAutoConfiguration — axis2.xml staging + // ═══════════════════════════════════════════════════════════════════════ + + @Test + void repoProperty_usedForAxis2XmlStaging(@TempDir Path tempDir) throws Exception { + // Pre-stage axis2.xml so staging is skipped (it checks for existing file) + Path confDir = tempDir.resolve("conf"); + Files.createDirectories(confDir); + Files.writeString(confDir.resolve("axis2.xml"), "<axisconfig/>"); + + Axis2Properties properties = new Axis2Properties(); + properties.setRepo(tempDir.toString()); + + ServletContext servletContext = mock(ServletContext.class); + // getRealPath returns null (embedded mode) + when(servletContext.getRealPath("/WEB-INF")).thenReturn(null); + + Axis2RepositoryAutoConfiguration config = new Axis2RepositoryAutoConfiguration(); + // Should not throw — axis2.xml already exists, staging is skipped. + // The implicit no-exception check is the assertion: if staging + // incorrectly tries to overwrite the existing file and fails, + // the test fails with an exception. + config.axis2RepositoryInitializer(properties).onStartup(servletContext); + } + + @Test + void nullRealPath_andEmptyRepo_logsWarning() throws Exception { + Axis2Properties properties = new Axis2Properties(); + // Both repo and getRealPath are empty/null + + ServletContext servletContext = mock(ServletContext.class); + when(servletContext.getRealPath("/WEB-INF")).thenReturn(null); + + Axis2RepositoryAutoConfiguration config = new Axis2RepositoryAutoConfiguration(); + // Should not throw — just logs a warning and skips staging + config.axis2RepositoryInitializer(properties).onStartup(servletContext); + // If we got here without exception, the graceful fallback works + } +} diff --git a/src/site/xdoc/docs/spring-boot-starter.xml b/src/site/xdoc/docs/spring-boot-starter.xml index d6666d4c9e..a416271fde 100644 --- a/src/site/xdoc/docs/spring-boot-starter.xml +++ b/src/site/xdoc/docs/spring-boot-starter.xml @@ -34,6 +34,44 @@ from a multi-day configuration project to a single Maven dependency. It handles servlet registration, repository configuration, and OpenAPI/MCP endpoint activation with sensible defaults — for both SOAP and JSON-RPC services.</p> +<h3>What Axis2 brings to Spring Boot</h3> + +<ul> +<li><strong>Large JSON streaming (10MB–1GB+)</strong> — the + <a href="json-streaming-formatter.html">streaming JSON formatter</a> + flushes HTTP/2 DATA frames every 64KB, keeping server memory flat + regardless of response size. No reactive stack required.</li> +<li><strong>Native HTTP/2 with backpressure</strong> — the + <a href="http2-transport-additions.html">H2TransportSender</a> + provides connection multiplexing, flow control, and ALPN negotiation + as a drop-in transport layer.</li> +<li><strong>AI-ready services (MCP)</strong> — Axis2 auto-generates an + <a href="json-rpc-mcp-guide.html">MCP tool catalog</a> from deployed + services. AI assistants discover and call your services as tools + without hand-authored definitions.</li> +<li><strong>Response field selection</strong> — the + <a href="json-streaming-formatter.html#Field_Selection"><code>?fields=</code></a> + query parameter lets clients request only the fields they need, + reducing payload size and AI token consumption at the serialization + layer with zero application code.</li> +<li><strong>OpenAPI schemas from Hibernate mappings</strong> — the + <a href="openapi-jpa-schema.html">JPA/Hibernate schema generator</a> + produces read and write JSON Schemas directly from + <code>@Entity</code> annotations or <code>.hbm.xml</code> files. + No hand-authored API contracts — the schema stays in sync with + the database model by construction.</li> +<li><strong>Built-in pagination</strong> — the + <a href="json-pagination.html"><code>PaginatedResponse</code></a> + wrapper provides offset/limit pagination with server-enforced + safety limits, <code>totalCount</code> for page controls, and + <code>hasMore</code> for infinite scroll — mapping directly to + JPA/Hibernate's <code>setFirstResult</code>/<code>setMaxResults</code>.</li> +<li><strong>SOAP + JSON from the same services</strong> — serve both + SOAP/XML and JSON-RPC from the same Spring beans, selected by + configuration. Useful for enterprises maintaining both legacy + and modern clients.</li> +</ul> + <p>This starter supports both <strong>WAR deployment</strong> to an external container (Tomcat 11, WildFly 32/39) and <strong>embedded Tomcat</strong> for local development. For embedded mode, set <code>axis2.repo</code> in <code>application.properties</code> @@ -164,13 +202,22 @@ are not tested.</p> <h3>Embedded mode limitations</h3> -<p>The current embedded Tomcat support requires a pre-built exploded WAR -on the filesystem (via <code>axis2.repo</code>). A future improvement -would be a <code>ClasspathBasedAxisConfigurator</code> that discovers -services and modules directly from the classpath, eliminating the need -for a filesystem layout entirely. Contributions are welcome — see the -<a href="https://github.com/apache/axis-axis2-java-core/blob/master/AXIS2_MODERNIZATION_PLAN.md">Axis2 Modernization Plan</a> -for the full roadmap.</p> +<p>Embedded Tomcat requires a pre-built exploded WAR on the filesystem +(pointed to by <code>axis2.repo</code>). This means you must run +<code>mvn install</code> before <code>mvn spring-boot:run</code> so +that the <code>.aar</code> service archives and <code>axis2.xml</code> +are staged in the build output directory. Changes to service metadata +(operations, message receivers, MCP descriptions) require a rebuild.</p> + +<p>Note that the service <em>code</em> itself is a Spring +<code>@Component</code> bean on the classpath — the <code>.aar</code> +file contains only metadata (<code>services.xml</code>) that tells +Axis2 which Spring bean to call, which operations to expose, and +which message receivers to use. The <code>SpringBeanName</code> +parameter in <code>services.xml</code> bridges Axis2 service +dispatch to Spring's application context. A future improvement +could scan for <code>services.xml</code> resources on the classpath, +eliminating the filesystem requirement entirely.</p> <!-- ============================================================ --> <a name="quickstart"/> @@ -199,10 +246,12 @@ for the full roadmap.</p> etc.) and eliminates the need for a hand-coded <code>Axis2WebAppInitializer</code> or <code>OpenApiServlet</code> class.</p> -<p>The starter auto-registers <code>AxisServlet</code> at <code>/services/*</code> -and — when <code>axis2-openapi</code> is on the classpath — registers the OpenAPI -servlet at <code>/openapi.json</code>, <code>/openapi.yaml</code>, -<code>/swagger-ui</code>, and <code>/openapi-mcp.json</code>.</p> +<p>The starter auto-registers <code>AxisServlet</code> at <code>/services/*</code>. +If you also add the optional <code>axis2-openapi</code> dependency (shown above), +the starter additionally registers endpoints for OpenAPI spec generation +(<code>/openapi.json</code>), Swagger UI (<code>/swagger-ui</code>), and MCP +tool discovery (<code>/openapi-mcp.json</code>). See +<a href="#openapi_mcp">Section 7</a> for details.</p> <!-- ============================================================ --> <a name="how_axisservlet_works"/> @@ -374,20 +423,66 @@ does not already exist. If your Maven build pre-stages it (e.g., via <a name="openapi_mcp"/> <h2>7. OpenAPI and MCP Support</h2> -<p>When <code>axis2-openapi</code> is on the classpath, the starter automatically -registers the OpenAPI servlet. No additional code is needed in the consuming app.</p> +<p><code>axis2-openapi</code> is a separate Axis2 module (Maven artifact: +<code>org.apache.axis2:axis2-openapi</code>) that auto-generates an +<a href="https://en.wikipedia.org/wiki/OpenAPI_Specification">OpenAPI</a> +3.0.1 specification and +<a href="https://en.wikipedia.org/wiki/Model_Context_Protocol">MCP</a> +(Model Context Protocol) tool catalog from your deployed Axis2 services. +OpenAPI is a standard format for describing REST APIs — tools like +<a href="https://swagger.io/tools/swagger-ui/">Swagger UI</a> use it to +generate interactive documentation. MCP is a protocol that lets AI +assistants (Claude, ChatGPT, etc.) discover and call your services +as tools.</p> + +<p>The module auto-generates both from your deployed Axis2 services. +It reads each service's <code>services.xml</code> (inside the <code>.aar</code> +file) at runtime — specifically the operation names, message receiver types, +and MCP metadata parameters — and produces the specification automatically. +No annotations on your service code are needed.</p> + +<p><strong>How it works with the starter:</strong> The starter detects +<code>axis2-openapi</code> on the classpath via Spring Boot's +<code>@ConditionalOnClass(OpenApiModule.class)</code> and automatically +registers a servlet that delegates to Axis2's +<a href="https://github.com/apache/axis-axis2-java-core/blob/master/modules/openapi/src/main/java/org/apache/axis2/openapi/SwaggerUIHandler.java">SwaggerUIHandler</a>. +No additional code is needed in your application.</p> + +<p>To enable it, add the dependency to your <code>pom.xml</code>:</p> + +<pre> +<dependency> + <groupId>org.apache.axis2</groupId> + <artifactId>axis2-openapi</artifactId> + <version>2.0.1</version> +</dependency> +</pre> + +<p>Your Maven build must also copy the <code>axis2-openapi</code> JAR as a +<code>.mar</code> (Module Archive) into <code>WEB-INF/modules/</code> so the +Axis2 engine can load it:</p> + +<pre> +<!-- In maven-antrun-plugin, prepare-package phase --> +<copy file="${settings.localRepository}/org/apache/axis2/axis2-openapi/${axis2.version}/axis2-openapi-${axis2.version}.jar" + tofile="${project.build.directory}/${project.build.finalName}/WEB-INF/modules/openapi-${axis2.version}.mar" + overwrite="true"/> +</pre> -<p>The servlet provides:</p> +<p>Once deployed, the following endpoints are available (no auth required):</p> <ul> -<li><code>GET /openapi.json</code> — OpenAPI 3.0.1 specification</li> +<li><code>GET /openapi.json</code> — OpenAPI 3.0.1 specification (JSON)</li> <li><code>GET /openapi.yaml</code> — same in YAML format</li> <li><code>GET /swagger-ui</code> — interactive Swagger UI</li> -<li><code>GET /openapi-mcp.json</code> — MCP tool catalog for AI assistants</li> +<li><code>GET /openapi-mcp.json</code> — MCP tool catalog for AI assistants + (includes operation names, input schemas, and payload templates)</li> </ul> -<p>See the <a href="json-rpc-mcp-guide.html">JSON-RPC MCP Integration Guide</a> -for details on the MCP catalog format, the Axis2 JSON-RPC envelope, and how -AI assistants discover and call Axis2 services.</p> +<p>The MCP catalog is generated from <code>services.xml</code> parameters +like <code>mcpDescription</code> and <code>mcpInputSchema</code>. +See the <a href="json-rpc-mcp-guide.html">JSON-RPC MCP Integration Guide</a> +for the full catalog format and how AI assistants discover and call Axis2 +services.</p> <!-- ============================================================ --> <a name="migration"/>
