From 325abe11f34794396fea317d1fae131413bb8c16 Mon Sep 17 00:00:00 2001 From: Jose Manzano Date: Mon, 23 Mar 2026 17:08:22 +0100 Subject: [PATCH 1/5] fix: align generated clients with Spring 7 and Jakarta validation Update BOAT templates so generated resttemplate clients use Spring 7-compatible HttpHeaders/UriComponentsBuilder APIs and emit Jakarta Email annotations when Jakarta EE is enabled. This removes the need for downstream post-generation text replacement workarounds in SSDK 21 projects. Made-with: Cursor --- .../libraries/resttemplate/ApiClient.mustache | 10 +++++----- .../templates/boat-spring/beanValidationCore.mustache | 4 ++-- .../boat-webhooks/beanValidationCore.mustache | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/boat-scaffold/src/main/templates/boat-java/libraries/resttemplate/ApiClient.mustache b/boat-scaffold/src/main/templates/boat-java/libraries/resttemplate/ApiClient.mustache index 27a976ded..200c7d1ea 100644 --- a/boat-scaffold/src/main/templates/boat-java/libraries/resttemplate/ApiClient.mustache +++ b/boat-scaffold/src/main/templates/boat-java/libraries/resttemplate/ApiClient.mustache @@ -307,7 +307,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { * @return ApiClient this client */ public ApiClient addDefaultHeader(String name, String value) { - if (defaultHeaders.containsKey(name)) { + if (defaultHeaders.containsHeader(name)) { defaultHeaders.remove(name); } defaultHeaders.add(name, value); @@ -688,7 +688,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { finalUri += "?" + queryUri; } String expandedPath = this.expandPath(finalUri, uriParams); - final UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(basePath).path(expandedPath); + final UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(basePath).path(expandedPath); URI uri; try { @@ -697,7 +697,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { throw new RestClientException("Could not build URL: " + builder.toUriString(), ex); } - final BodyBuilder requestBuilder = RequestEntity.method(method, UriComponentsBuilder.fromHttpUrl(basePath).toUriString() + finalUri, uriParams); + final BodyBuilder requestBuilder = RequestEntity.method(method, UriComponentsBuilder.fromUriString(basePath).toUriString() + finalUri, uriParams); if (accept != null) { requestBuilder.accept(accept.toArray(new MediaType[accept.size()])); } @@ -728,7 +728,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { * @param requestBuilder The current request */ protected void addHeadersToRequest(HttpHeaders headers, BodyBuilder requestBuilder) { - for (Entry> entry : headers.entrySet()) { + for (Entry> entry : headers.headerSet()) { List values = entry.getValue(); for (String value : values) { if (value != null) { @@ -841,7 +841,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { return ""; } StringBuilder builder = new StringBuilder(); - for (Entry> entry : headers.entrySet()) { + for (Entry> entry : headers.headerSet()) { builder.append(entry.getKey()).append("=["); for (String value : entry.getValue()) { builder.append(value).append(","); diff --git a/boat-scaffold/src/main/templates/boat-spring/beanValidationCore.mustache b/boat-scaffold/src/main/templates/boat-spring/beanValidationCore.mustache index af85b9dd3..18ecc3138 100644 --- a/boat-scaffold/src/main/templates/boat-spring/beanValidationCore.mustache +++ b/boat-scaffold/src/main/templates/boat-spring/beanValidationCore.mustache @@ -13,9 +13,9 @@ minLength not set, maxLength set @Size: minItems not set && maxItems set }}{{^minItems}}{{#maxItems}}@Size(max = {{.}}) {{/maxItems}}{{/minItems}}{{! @Email: useBeanValidation set && isEmail && java8 set -}}{{#useBeanValidation}}{{#isEmail}}{{#java8}}@org.hibernate.validator.constraints.Email{{/java8}}{{/isEmail}}{{/useBeanValidation}}{{! +}}{{#useBeanValidation}}{{#isEmail}}{{#useJakartaEe}}@jakarta.validation.constraints.Email{{/useJakartaEe}}{{^useJakartaEe}}{{#java8}}@org.hibernate.validator.constraints.Email{{/java8}}{{/useJakartaEe}}{{/isEmail}}{{/useBeanValidation}}{{! @Email: performBeanValidation set && isEmail && not java8 set -}}{{#performBeanValidation}}{{#isEmail}}{{^java8}}@jakarta.validation.constraints.Email{{/java8}}{{/isEmail}}{{/performBeanValidation}}{{! +}}{{#performBeanValidation}}{{#isEmail}}{{#useJakartaEe}}@jakarta.validation.constraints.Email{{/useJakartaEe}}{{^useJakartaEe}}{{^java8}}@javax.validation.constraints.Email{{/java8}}{{/useJakartaEe}}{{/isEmail}}{{/performBeanValidation}}{{! check for integer or long / all others=decimal type with @Decimal* isInteger set }}{{#isInteger}}{{#minimum}}@Min({{.}}) {{/minimum}}{{#maximum}}@Max({{.}}) {{/maximum}}{{/isInteger}}{{! diff --git a/boat-scaffold/src/main/templates/boat-webhooks/beanValidationCore.mustache b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationCore.mustache index 7faf96bc9..f7ded58bc 100644 --- a/boat-scaffold/src/main/templates/boat-webhooks/beanValidationCore.mustache +++ b/boat-scaffold/src/main/templates/boat-webhooks/beanValidationCore.mustache @@ -12,9 +12,9 @@ minLength not set, maxLength set @Size: minItems not set && maxItems set }}{{^minItems}}{{#maxItems}}@Size(max = {{.}}) {{/maxItems}}{{/minItems}}{{! @Email: useBeanValidation set && isEmail && java8 set -}}{{#useBeanValidation}}{{#isEmail}}{{#java8}}@org.hibernate.validator.constraints.Email{{/java8}}{{/isEmail}}{{/useBeanValidation}}{{! +}}{{#useBeanValidation}}{{#isEmail}}{{#useJakartaEe}}@jakarta.validation.constraints.Email{{/useJakartaEe}}{{^useJakartaEe}}{{#java8}}@org.hibernate.validator.constraints.Email{{/java8}}{{/useJakartaEe}}{{/isEmail}}{{/useBeanValidation}}{{! @Email: performBeanValidation set && isEmail && not java8 set -}}{{#performBeanValidation}}{{#isEmail}}{{^java8}}@jakarta.validation.constraints.Email{{/java8}}{{/isEmail}}{{/performBeanValidation}}{{! +}}{{#performBeanValidation}}{{#isEmail}}{{#useJakartaEe}}@jakarta.validation.constraints.Email{{/useJakartaEe}}{{^useJakartaEe}}{{^java8}}@javax.validation.constraints.Email{{/java8}}{{/useJakartaEe}}{{/isEmail}}{{/performBeanValidation}}{{! check for integer or long / all others=decimal type with @Decimal* isInteger set }}{{#isInteger}}{{#minimum}}@Min({{.}}) {{/minimum}}{{#maximum}}@Max({{.}}) {{/maximum}}{{/isInteger}}{{! From be3fb270073c22e9335ac8bd660f6a843cda6486 Mon Sep 17 00:00:00 2001 From: Jose Manzano Date: Mon, 23 Mar 2026 17:35:32 +0100 Subject: [PATCH 2/5] fix: update header handling to use HttpHeaders methods --- boat-maven-plugin/pom.xml | 12 ++++++++++++ boat-scaffold/pom.xml | 1 + .../libraries/resttemplate/ApiClient.mustache | 17 ++++++++--------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/boat-maven-plugin/pom.xml b/boat-maven-plugin/pom.xml index 7286b5300..e4c895a22 100644 --- a/boat-maven-plugin/pom.xml +++ b/boat-maven-plugin/pom.xml @@ -53,6 +53,18 @@ org.openapitools.openapidiff openapi-diff-core 2.1.7 + + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + diff --git a/boat-scaffold/pom.xml b/boat-scaffold/pom.xml index d62bb8924..8db6c366e 100644 --- a/boat-scaffold/pom.xml +++ b/boat-scaffold/pom.xml @@ -161,6 +161,7 @@ org.apache.maven.resolver maven-resolver-transport-http ${maven.resolver.version} + test com.github.javaparser diff --git a/boat-scaffold/src/main/templates/boat-java/libraries/resttemplate/ApiClient.mustache b/boat-scaffold/src/main/templates/boat-java/libraries/resttemplate/ApiClient.mustache index 200c7d1ea..4d9651b9c 100644 --- a/boat-scaffold/src/main/templates/boat-java/libraries/resttemplate/ApiClient.mustache +++ b/boat-scaffold/src/main/templates/boat-java/libraries/resttemplate/ApiClient.mustache @@ -307,7 +307,7 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { * @return ApiClient this client */ public ApiClient addDefaultHeader(String name, String value) { - if (defaultHeaders.containsHeader(name)) { + if (defaultHeaders.toSingleValueMap().containsKey(name)) { defaultHeaders.remove(name); } defaultHeaders.add(name, value); @@ -728,14 +728,13 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { * @param requestBuilder The current request */ protected void addHeadersToRequest(HttpHeaders headers, BodyBuilder requestBuilder) { - for (Entry> entry : headers.headerSet()) { - List values = entry.getValue(); + headers.forEach((key, values) -> { for (String value : values) { if (value != null) { - requestBuilder.header(entry.getKey(), value); + requestBuilder.header(key, value); } } - } + }); } /** @@ -841,14 +840,14 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { return ""; } StringBuilder builder = new StringBuilder(); - for (Entry> entry : headers.headerSet()) { - builder.append(entry.getKey()).append("=["); - for (String value : entry.getValue()) { + headers.forEach((key, values) -> { + builder.append(key).append("=["); + for (String value : values) { builder.append(value).append(","); } builder.setLength(builder.length() - 1); // Get rid of trailing comma builder.append("],"); - } + }); builder.setLength(builder.length() - 1); // Get rid of trailing comma return builder.toString(); } From 00ad9a848d296213d5f0bdaf1c2ab3c3f3447120 Mon Sep 17 00:00:00 2001 From: Jose Manzano Date: Mon, 23 Mar 2026 17:55:59 +0100 Subject: [PATCH 3/5] test: add regression guards for Spring compatibility Add explicit resttemplate generation assertions so ApiClient stays compatible with both Spring 5 and Spring 7 HttpHeaders APIs. Add POM guard tests to keep classpath-isolation fixes in place and prevent ClassRealm regressions in plugin CI. --- .../oss/boat/DependencyGuardTests.java | 56 +++++++++++++++++++ .../oss/codegen/DependencyGuardTests.java | 41 ++++++++++++++ .../codegen/java/BoatJavaCodeGenTests.java | 36 ++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 boat-maven-plugin/src/test/java/com/backbase/oss/boat/DependencyGuardTests.java create mode 100644 boat-scaffold/src/test/java/com/backbase/oss/codegen/DependencyGuardTests.java diff --git a/boat-maven-plugin/src/test/java/com/backbase/oss/boat/DependencyGuardTests.java b/boat-maven-plugin/src/test/java/com/backbase/oss/boat/DependencyGuardTests.java new file mode 100644 index 000000000..3bdca6ca5 --- /dev/null +++ b/boat-maven-plugin/src/test/java/com/backbase/oss/boat/DependencyGuardTests.java @@ -0,0 +1,56 @@ +package com.backbase.oss.boat; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +class DependencyGuardTests { + + @Test + void shouldExcludeApacheHttpClientFromOpenApiDiffCoreDependency() throws Exception { + Document pom = readPom(Path.of("pom.xml")); + XPath xpath = XPathFactory.newInstance().newXPath(); + + String dependencySelector = + "/project/dependencies/dependency[groupId='org.openapitools.openapidiff' and artifactId='openapi-diff-core']"; + Node dependencyNode = (Node) xpath.evaluate(dependencySelector, pom, XPathConstants.NODE); + assertNotNull(dependencyNode, "openapi-diff-core dependency must exist in boat-maven-plugin/pom.xml"); + + NodeList excludedArtifacts = (NodeList) xpath.evaluate( + dependencySelector + "/exclusions/exclusion[groupId='org.apache.httpcomponents']/artifactId", + pom, + XPathConstants.NODESET + ); + + Set exclusions = new HashSet<>(); + for (int i = 0; i < excludedArtifacts.getLength(); i++) { + exclusions.add(excludedArtifacts.item(i).getTextContent().trim()); + } + + assertEquals(2, exclusions.size(), "Exactly two Apache HttpComponents exclusions are expected"); + assertTrue(exclusions.contains("httpclient"), "httpclient must be excluded to avoid ClassRealm conflicts"); + assertTrue(exclusions.contains("httpcore"), "httpcore must be excluded to avoid ClassRealm conflicts"); + } + + private static Document readPom(Path path) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + return factory.newDocumentBuilder().parse(path.toFile()); + } +} diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/DependencyGuardTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/DependencyGuardTests.java new file mode 100644 index 000000000..9aa0c3db2 --- /dev/null +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/DependencyGuardTests.java @@ -0,0 +1,41 @@ +package com.backbase.oss.codegen; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.nio.file.Path; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +class DependencyGuardTests { + + @Test + void shouldKeepResolverHttpTransportAsTestScope() throws Exception { + Document pom = readPom(Path.of("pom.xml")); + XPath xpath = XPathFactory.newInstance().newXPath(); + + String dependencySelector = + "/project/dependencies/dependency[groupId='org.apache.maven.resolver' and artifactId='maven-resolver-transport-http']"; + + Node dependencyNode = (Node) xpath.evaluate(dependencySelector, pom, XPathConstants.NODE); + assertNotNull(dependencyNode, "maven-resolver-transport-http dependency must exist in boat-scaffold/pom.xml"); + + String scope = xpath.evaluate(dependencySelector + "/scope/text()", pom); + assertEquals("test", scope.trim(), "maven-resolver-transport-http must remain test-scoped"); + } + + private static Document readPom(Path path) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + return factory.newDocumentBuilder().parse(path.toFile()); + } +} diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatJavaCodeGenTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatJavaCodeGenTests.java index 1dd8c6215..f827da438 100644 --- a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatJavaCodeGenTests.java +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatJavaCodeGenTests.java @@ -9,8 +9,10 @@ import static com.backbase.oss.codegen.java.BoatJavaCodeGen.USE_WITH_MODIFIERS; import static java.util.stream.Collectors.groupingBy; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -22,6 +24,7 @@ import io.swagger.v3.parser.core.models.ParseOptions; import java.io.File; import java.io.FileNotFoundException; +import java.nio.file.Files; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -208,4 +211,37 @@ void shouldHonourBeanValidationOption(boolean useBeanValidation) throws FileNotF assertThat("Expect jakarta Valid import", compilationUnit.getImports().stream().anyMatch( id -> id.getNameAsString().equals("jakarta.validation.Valid")), is(useBeanValidation)); } + + @Test + void shouldGenerateRestTemplateApiClientCompatibleWithSpring5AndSpring7() throws Exception { + var input = new File("src/test/resources/boat-spring/openapi.yaml"); + var output = TEST_OUTPUT + "/shouldGenerateRestTemplateApiClientCompatibleWithSpring5AndSpring7"; + + final BoatJavaCodeGen gen = new BoatJavaCodeGen(); + gen.setOutputDir(output); + gen.setInputSpec(input.getAbsolutePath()); + gen.setApiPackage("com.backbase.test.api"); + gen.setModelPackage("com.backbase.test.api.model"); + gen.setInvokerPackage("com.backbase.test.api.invoker"); + gen.additionalProperties().put("library", "resttemplate"); + + var openApiInput = new OpenAPIParser() + .readLocation(input.getAbsolutePath(), null, new ParseOptions()) + .getOpenAPI(); + var clientOptInput = new ClientOptInput(); + clientOptInput.config(gen); + clientOptInput.openAPI(openApiInput); + + List files = new DefaultGenerator().opts(clientOptInput).generate(); + File apiClientFile = files.stream() + .filter(file -> file.getName().equals("ApiClient.java")) + .findFirst() + .orElseThrow(() -> new IllegalStateException("ApiClient.java was not generated")); + + String apiClientCode = Files.readString(apiClientFile.toPath()); + assertThat(apiClientCode, containsString("defaultHeaders.toSingleValueMap().containsKey(name)")); + assertThat(apiClientCode, containsString("headers.forEach((key, values) -> {")); + assertThat(apiClientCode, not(containsString("containsHeader("))); + assertThat(apiClientCode, not(containsString("headerSet("))); + } } From b125c3927524dc83e8c4d5f6f595aebd51080cb3 Mon Sep 17 00:00:00 2001 From: Jose Manzano Date: Mon, 23 Mar 2026 18:04:15 +0100 Subject: [PATCH 4/5] fix: update spring-core version to 6.2.11 for CVE-2025-41249 --- boat-maven-plugin/src/it/example/pom.xml | 2 +- .../backbase/oss/boat/DependencyGuardTests.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/boat-maven-plugin/src/it/example/pom.xml b/boat-maven-plugin/src/it/example/pom.xml index 650561ff3..5ac016ac2 100644 --- a/boat-maven-plugin/src/it/example/pom.xml +++ b/boat-maven-plugin/src/it/example/pom.xml @@ -153,7 +153,7 @@ org.springframework spring-core - 6.1.14 + 6.2.11 compile diff --git a/boat-maven-plugin/src/test/java/com/backbase/oss/boat/DependencyGuardTests.java b/boat-maven-plugin/src/test/java/com/backbase/oss/boat/DependencyGuardTests.java index 3bdca6ca5..47fbbb98a 100644 --- a/boat-maven-plugin/src/test/java/com/backbase/oss/boat/DependencyGuardTests.java +++ b/boat-maven-plugin/src/test/java/com/backbase/oss/boat/DependencyGuardTests.java @@ -44,6 +44,20 @@ void shouldExcludeApacheHttpClientFromOpenApiDiffCoreDependency() throws Excepti assertTrue(exclusions.contains("httpcore"), "httpcore must be excluded to avoid ClassRealm conflicts"); } + @Test + void shouldKeepPatchedSpringCoreVersionInInvokerExample() throws Exception { + Document pom = readPom(Path.of("src/it/example/pom.xml")); + XPath xpath = XPathFactory.newInstance().newXPath(); + + String springCoreVersion = xpath.evaluate( + "/project/dependencyManagement/dependencies/dependency[groupId='org.springframework' and artifactId='spring-core']/version/text()", + pom + ); + + assertEquals("6.2.11", springCoreVersion.trim(), + "Invoker example must keep patched spring-core version to avoid CVE-2025-41249"); + } + private static Document readPom(Path path) throws Exception { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); From fe6025b6c0d47be39b358bd102152340dd8b0525 Mon Sep 17 00:00:00 2001 From: Jose Manzano Date: Mon, 23 Mar 2026 18:08:52 +0100 Subject: [PATCH 5/5] doc: update README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 5fe9e74ec..009ce8c03 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ It currently consists of # Release Notes BOAT is still under development and subject to change. +## 0.17.76 +* Updated RestTemplate Java client templates for Spring compatibility by using header APIs that work with both Spring 5 and Spring 7. +* Updated bean validation templates (`boat-spring` and `boat-webhooks`) to generate Jakarta `@Email` annotations when `useJakartaEe=true`. +* Updated `spring-core` in plugin IT examples to `6.2.11` to address CVE-2025-41249, with an explicit guard test to keep the patched version. + ## 0.17.75 * Fixed duplicate serialization of the discriminator property in Jackson-based Java models by removing allowGetters = true from the @JsonIgnoreProperties annotation. * In Spring generator added support for type-level validation in collections via the `x-not-null` vendor extension to allow `@NotNull` annotations on generic type arguments.