Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions boat-maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@
<groupId>org.openapitools.openapidiff</groupId>
<artifactId>openapi-diff-core</artifactId>
<version>2.1.7</version>
<exclusions>
<!-- Avoid loading Apache HttpClient in plugin realm to prevent
ClassRealm conflicts with Maven's own transport stack. -->
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
Expand Down
2 changes: 1 addition & 1 deletion boat-maven-plugin/src/it/example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.1.14</version>
<version>6.2.11</version>
<scope>compile</scope>
</dependency>

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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());
}
}
1 change: 1 addition & 0 deletions boat-scaffold/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-transport-http</artifactId>
<version>${maven.resolver.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand All @@ -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()]));
}
Expand Down Expand Up @@ -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<String, List<String>> entry : headers.entrySet()) {
List<String> values = entry.getValue();
headers.forEach((key, values) -> {
for (String value : values) {
if (value != null) {
requestBuilder.header(entry.getKey(), value);
requestBuilder.header(key, value);
}
}
}
});
}

/**
Expand Down Expand Up @@ -841,14 +840,14 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
return "";
}
StringBuilder builder = new StringBuilder();
for (Entry<String, List<String>> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}{{!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}{{!
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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<File> 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(")));
}
}