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.
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-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
new file mode 100644
index 000000000..47fbbb98a
--- /dev/null
+++ b/boat-maven-plugin/src/test/java/com/backbase/oss/boat/DependencyGuardTests.java
@@ -0,0 +1,70 @@
+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");
+ }
+
+ @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);
+ 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/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 27a976ded..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.containsKey(name)) {
+ if (defaultHeaders.toSingleValueMap().containsKey(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,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.entrySet()) {
- 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.entrySet()) {
- 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();
}
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}}{{!
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(")));
+ }
}