From ac958f0c935cfe55c29bc38833467f7693f63f5e Mon Sep 17 00:00:00 2001 From: Angela Boakes Date: Wed, 1 Apr 2026 17:15:02 +0100 Subject: [PATCH 1/3] chore: bump to 0.18.1-SNAPSHOT and fix spring example escaping Update project versions to 0.18.1-SNAPSHOT, add 0.18.1 release notes, and fix boat-spring ExampleObject value rendering by unwrapping escaped quotes with regression coverage. Made-with: Cursor --- README.md | 5 +++ boat-engine/pom.xml | 2 +- boat-maven-plugin/pom.xml | 2 +- boat-quay/boat-quay-lint/pom.xml | 2 +- boat-quay/boat-quay-rules/pom.xml | 2 +- boat-quay/pom.xml | 2 +- boat-scaffold/pom.xml | 4 +- .../oss/codegen/java/BoatSpringCodeGen.java | 19 +++++++++ .../main/templates/boat-spring/api.mustache | 2 +- .../codegen/java/BoatSpringCodeGenTests.java | 40 +++++++++++++++++++ boat-trail-resources/pom.xml | 2 +- pom.xml | 2 +- tests/pom.xml | 2 +- 13 files changed, 75 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d466e37ac..d3eced9a5 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.18.1 +* Spring generator: fixed `@ExampleObject` rendering in `api.mustache` by unwrapping escaped quotes in example payloads. +* Added `unwrapEscapedQuotes` lambda to `boat-spring` generator templates to prevent malformed annotation values (for example `value = "\"{...}"`). +* Added a regression test to verify generated Spring API interfaces include valid `@ExampleObject` annotation values and remain parseable Java code. + ## 0.18.0 * openapi-generator `7.20.0` baseline (Spring Boot 4, Jackson 3) * moved Java and JavaSpring templates to `7.20.0` adding remaing custom features diff --git a/boat-engine/pom.xml b/boat-engine/pom.xml index 776bed88a..89746680a 100644 --- a/boat-engine/pom.xml +++ b/boat-engine/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-engine jar diff --git a/boat-maven-plugin/pom.xml b/boat-maven-plugin/pom.xml index 3c8e8aae0..00069a4aa 100644 --- a/boat-maven-plugin/pom.xml +++ b/boat-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-maven-plugin diff --git a/boat-quay/boat-quay-lint/pom.xml b/boat-quay/boat-quay-lint/pom.xml index 14df40cd7..db9b4ebf6 100644 --- a/boat-quay/boat-quay-lint/pom.xml +++ b/boat-quay/boat-quay-lint/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss boat-quay - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-quay-lint diff --git a/boat-quay/boat-quay-rules/pom.xml b/boat-quay/boat-quay-rules/pom.xml index 0f48c13c5..609a524fd 100644 --- a/boat-quay/boat-quay-rules/pom.xml +++ b/boat-quay/boat-quay-rules/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss boat-quay - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-quay-rules diff --git a/boat-quay/pom.xml b/boat-quay/pom.xml index 7a2d7f695..277df7c25 100644 --- a/boat-quay/pom.xml +++ b/boat-quay/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT diff --git a/boat-scaffold/pom.xml b/boat-scaffold/pom.xml index 479e70c16..6186671b7 100644 --- a/boat-scaffold/pom.xml +++ b/boat-scaffold/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-scaffold @@ -102,7 +102,7 @@ com.backbase.oss boat-trail-resources - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT test diff --git a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java index 633651516..bd03dc4b1 100644 --- a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java +++ b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java @@ -48,6 +48,7 @@ public class BoatSpringCodeGen extends SpringCodegen { public static final String ADD_SERVLET_REQUEST = "addServletRequest"; public static final String ADD_BINDING_RESULT = "addBindingResult"; + public static final String UNWRAP_ESCAPED_QUOTES = "unwrapEscapedQuotes"; private static final String VENDOR_EXTENSION_NOT_NULL = "x-not-null"; @@ -147,6 +148,23 @@ protected String postProcessLine(String line) { } } + static class UnwrapEscapedQuotes implements Mustache.Lambda { + + @Override + public void execute(Fragment frag, Writer out) throws IOException { + String text = frag.execute(); + if (text == null) { + return; + } + String normalized = text.replace("\\\\\"", "\\\""); + if (normalized.length() >= 4 && normalized.startsWith("\\\"") && normalized.endsWith("\\\"")) { + out.write(normalized.substring(2, normalized.length() - 2)); + return; + } + out.write(normalized); + } + } + /** * Adds a HttpServletRequest object to the API definition method. */ @@ -332,6 +350,7 @@ public void processOpts() { this.additionalProperties.put("newLine8", new NewLineIndent(8, " ")); this.additionalProperties.put("toOneLine", new FormatToOneLine()); this.additionalProperties.put("trimAndIndent4", new TrimAndIndent(4, " ")); + this.additionalProperties.put(UNWRAP_ESCAPED_QUOTES, new UnwrapEscapedQuotes()); } @Override diff --git a/boat-scaffold/src/main/templates/boat-spring/api.mustache b/boat-scaffold/src/main/templates/boat-spring/api.mustache index 9f3ac9721..41500ef63 100644 --- a/boat-scaffold/src/main/templates/boat-spring/api.mustache +++ b/boat-scaffold/src/main/templates/boat-spring/api.mustache @@ -187,7 +187,7 @@ public interface {{classname}} { {{#examples}} @ExampleObject( name = "{{{exampleName}}}", - value = "{{{exampleValue}}}" + value = "{{#unwrapEscapedQuotes}}{{{exampleValue}}}{{/unwrapEscapedQuotes}}" ){{^-last}},{{/-last}} {{/examples}} {{#-last}} diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java index c0c52fb20..8f64fb925 100644 --- a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java @@ -6,6 +6,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isA; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -139,6 +140,45 @@ void multipartWithFileAndObject() throws IOException { assertThat(filesParam.getTypeAsString(), equalTo("List")); } + @Test + void shouldGenerateValidExampleObjectAnnotation() throws IOException { + var codegen = new BoatSpringCodeGen(); + var input = new File("src/test/resources/openapi-with-examples/openapi-with-multiple-permissions.yaml"); + codegen.setLibrary("spring-boot"); + codegen.setInterfaceOnly(true); + codegen.setSkipDefaultInterface(true); + codegen.setOutputDir(TEST_OUTPUT + "/example-object"); + codegen.setInputSpec(input.getAbsolutePath()); + codegen.additionalProperties().put(SpringCodegen.USE_SPRING_BOOT3, Boolean.TRUE.toString()); + + var openApiInput = new OpenAPIParser().readLocation(input.getAbsolutePath(), null, new ParseOptions()) + .getOpenAPI(); + var clientOptInput = new ClientOptInput(); + clientOptInput.config(codegen); + clientOptInput.openAPI(openApiInput); + + List files = new DefaultGenerator().opts(clientOptInput).generate(); + + File apiFile = files.stream() + .filter(file -> file.getName().endsWith("Api.java")) + .filter(file -> { + try { + return Files.readString(file.toPath()).contains("@ExampleObject("); + } catch (IOException e) { + throw new UnhandledException(e); + } + }) + .findFirst() + .orElseThrow(); + + String apiContent = Files.readString(apiFile.toPath()); + assertTrue(apiContent.contains("@ExampleObject(")); + assertTrue(apiContent.contains("Value Exceeded. Must be between {min} and {max}.")); + assertTrue(apiContent.contains("Bad Request")); + assertFalse(apiContent.contains("value = \"\\\"{")); + StaticJavaParser.parse(apiFile); + } + @Test void testReplaceBeanValidationCollectionType() { var codegen = new BoatSpringCodeGen(); diff --git a/boat-trail-resources/pom.xml b/boat-trail-resources/pom.xml index b3e0c7f8f..082a6947e 100644 --- a/boat-trail-resources/pom.xml +++ b/boat-trail-resources/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-trail-resources diff --git a/pom.xml b/pom.xml index bce873435..b140018c7 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT pom Backbase Open Api Tools is a collection of tools to work with Open API diff --git a/tests/pom.xml b/tests/pom.xml index 90eee5d9c..116bc34b9 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT tests From 4c14c4a03f78db9a0a4943197d79971d1bd1d766 Mon Sep 17 00:00:00 2001 From: Angela Boakes Date: Thu, 2 Apr 2026 09:31:39 +0100 Subject: [PATCH 2/3] test: increase UnwrapEscapedQuotes execute coverage Add focused unit tests for null input, wrapped escaped quotes, normalization-only behavior, and short token handling to satisfy new-code coverage expectations. Made-with: Cursor --- .../codegen/java/BoatSpringCodeGenTests.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java index 8f64fb925..4676fb5f1 100644 --- a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java @@ -98,6 +98,58 @@ void newLineIndent() throws IOException { assertThat(output.toString(), equalTo(String.format("__%n__Good%n__ morning,%n__ Dave%n"))); } + @Test + void unwrapEscapedQuotes_withNullInput_shouldWriteNothing() throws IOException { + final BoatSpringCodeGen.UnwrapEscapedQuotes lambda = new BoatSpringCodeGen.UnwrapEscapedQuotes(); + final StringWriter output = new StringWriter(); + final Fragment frag = mock(Fragment.class); + + when(frag.execute()).thenReturn(null); + + lambda.execute(frag, output); + + assertThat(output.toString(), equalTo("")); + } + + @Test + void unwrapEscapedQuotes_withWrappedEscapedQuotes_shouldRemoveOuterEscapedQuotes() throws IOException { + final BoatSpringCodeGen.UnwrapEscapedQuotes lambda = new BoatSpringCodeGen.UnwrapEscapedQuotes(); + final StringWriter output = new StringWriter(); + final Fragment frag = mock(Fragment.class); + + when(frag.execute()).thenReturn("\\\"{\\\"message\\\":\\\"Bad Request\\\"}\\\""); + + lambda.execute(frag, output); + + assertThat(output.toString(), equalTo("{\\\"message\\\":\\\"Bad Request\\\"}")); + } + + @Test + void unwrapEscapedQuotes_withDoubleEscapedQuotesAndNoWrapper_shouldOnlyNormalize() throws IOException { + final BoatSpringCodeGen.UnwrapEscapedQuotes lambda = new BoatSpringCodeGen.UnwrapEscapedQuotes(); + final StringWriter output = new StringWriter(); + final Fragment frag = mock(Fragment.class); + + when(frag.execute()).thenReturn("prefix\\\\\"quoted\\\\\"suffix"); + + lambda.execute(frag, output); + + assertThat(output.toString(), equalTo("prefix\\\"quoted\\\"suffix")); + } + + @Test + void unwrapEscapedQuotes_withShortEscapedQuoteToken_shouldNotUnwrap() throws IOException { + final BoatSpringCodeGen.UnwrapEscapedQuotes lambda = new BoatSpringCodeGen.UnwrapEscapedQuotes(); + final StringWriter output = new StringWriter(); + final Fragment frag = mock(Fragment.class); + + when(frag.execute()).thenReturn("\\\""); + + lambda.execute(frag, output); + + assertThat(output.toString(), equalTo("\\\"")); + } + @Test void addServletRequestTestFromOperation(){ final BoatSpringCodeGen gen = new BoatSpringCodeGen(); From 2fdbe37f227cc05f8755817e9d54ea7aac5cd32e Mon Sep 17 00:00:00 2001 From: Angela Boakes Date: Thu, 2 Apr 2026 10:12:26 +0100 Subject: [PATCH 3/3] test: parameterize unwrapEscapedQuotes scenarios Refactor duplicate UnwrapEscapedQuotes execute tests into a MethodSource-based parameterized test and keep the case provider in the helper section for maintainability. Made-with: Cursor --- .../codegen/java/BoatSpringCodeGenTests.java | 59 +++++-------------- 1 file changed, 16 insertions(+), 43 deletions(-) diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java index 4676fb5f1..1d24004c4 100644 --- a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java @@ -49,6 +49,7 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.UnhandledException; import org.hamcrest.Matchers; @@ -56,7 +57,9 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; import org.openapitools.codegen.CliOption; import org.openapitools.codegen.ClientOptInput; import org.openapitools.codegen.CodegenOperation; @@ -98,56 +101,18 @@ void newLineIndent() throws IOException { assertThat(output.toString(), equalTo(String.format("__%n__Good%n__ morning,%n__ Dave%n"))); } - @Test - void unwrapEscapedQuotes_withNullInput_shouldWriteNothing() throws IOException { - final BoatSpringCodeGen.UnwrapEscapedQuotes lambda = new BoatSpringCodeGen.UnwrapEscapedQuotes(); - final StringWriter output = new StringWriter(); - final Fragment frag = mock(Fragment.class); - - when(frag.execute()).thenReturn(null); - - lambda.execute(frag, output); - - assertThat(output.toString(), equalTo("")); - } - - @Test - void unwrapEscapedQuotes_withWrappedEscapedQuotes_shouldRemoveOuterEscapedQuotes() throws IOException { - final BoatSpringCodeGen.UnwrapEscapedQuotes lambda = new BoatSpringCodeGen.UnwrapEscapedQuotes(); - final StringWriter output = new StringWriter(); - final Fragment frag = mock(Fragment.class); - - when(frag.execute()).thenReturn("\\\"{\\\"message\\\":\\\"Bad Request\\\"}\\\""); - - lambda.execute(frag, output); - - assertThat(output.toString(), equalTo("{\\\"message\\\":\\\"Bad Request\\\"}")); - } - - @Test - void unwrapEscapedQuotes_withDoubleEscapedQuotesAndNoWrapper_shouldOnlyNormalize() throws IOException { - final BoatSpringCodeGen.UnwrapEscapedQuotes lambda = new BoatSpringCodeGen.UnwrapEscapedQuotes(); - final StringWriter output = new StringWriter(); - final Fragment frag = mock(Fragment.class); - - when(frag.execute()).thenReturn("prefix\\\\\"quoted\\\\\"suffix"); - - lambda.execute(frag, output); - - assertThat(output.toString(), equalTo("prefix\\\"quoted\\\"suffix")); - } - - @Test - void unwrapEscapedQuotes_withShortEscapedQuoteToken_shouldNotUnwrap() throws IOException { + @ParameterizedTest + @MethodSource("unwrapEscapedQuotesCases") + void unwrapEscapedQuotes_execute_shouldHandleAllScenarios(String input, String expectedOutput) throws IOException { final BoatSpringCodeGen.UnwrapEscapedQuotes lambda = new BoatSpringCodeGen.UnwrapEscapedQuotes(); final StringWriter output = new StringWriter(); final Fragment frag = mock(Fragment.class); - when(frag.execute()).thenReturn("\\\""); + when(frag.execute()).thenReturn(input); lambda.execute(frag, output); - assertThat(output.toString(), equalTo("\\\"")); + assertThat(output.toString(), equalTo(expectedOutput)); } @Test @@ -644,4 +609,12 @@ private static void assertMethodCollectionReturnType(MethodDeclaration method, S .getTypeArguments().get().getFirst().get(); assertEquals(itemType, collectionItemType.getName().toString()); } + + static Stream unwrapEscapedQuotesCases() { + return Stream.of( + Arguments.of((String) null, ""), + Arguments.of("\\\"{\\\"message\\\":\\\"Bad Request\\\"}\\\"", "{\\\"message\\\":\\\"Bad Request\\\"}"), + Arguments.of("prefix\\\\\"quoted\\\\\"suffix", "prefix\\\"quoted\\\"suffix"), + Arguments.of("\\\"", "\\\"")); + } }