Skip to content

Commit 99204ec

Browse files
smassetwing328
authored andcommitted
Avoid generating uncompilable response body in Spring's API template (#2903)
* Moved example string to a dedicated variable in Spring's methodBody template * Created a new exampleString template for JavaSpring * Added a new mustache lambda to trim whitespace in fragments * Added a new lambda to split long fragments into compilable strings * Use newly introduced lambdas in Spring's API template to avoid generating uncompilable example code
1 parent 21a291f commit 99204ec

70 files changed

Lines changed: 715 additions & 195 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
2727
import org.openapitools.codegen.languages.features.OptionalFeatures;
2828
import org.openapitools.codegen.languages.features.PerformBeanValidationFeatures;
29+
import org.openapitools.codegen.templating.mustache.SplitStringLambda;
30+
import org.openapitools.codegen.templating.mustache.TrimWhitespaceLambda;
2931
import org.openapitools.codegen.utils.URLPathUtils;
3032
import org.slf4j.Logger;
3133
import org.slf4j.LoggerFactory;
@@ -463,6 +465,10 @@ public void processOpts() {
463465
(Mustache.Lambda) (fragment, writer) -> writer.write(fragment.execute().replaceAll("\"", Matcher.quoteReplacement("\\\""))));
464466
additionalProperties.put("lambdaRemoveLineBreak",
465467
(Mustache.Lambda) (fragment, writer) -> writer.write(fragment.execute().replaceAll("\\r|\\n", "")));
468+
469+
additionalProperties.put("lambdaTrimWhitespace", new TrimWhitespaceLambda());
470+
471+
additionalProperties.put("lambdaSplitString", new SplitStringLambda());
466472
}
467473

468474
@Override
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
3+
* Copyright 2018 SmartBear Software
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.openapitools.codegen.templating.mustache;
19+
20+
import java.io.IOException;
21+
import java.io.Writer;
22+
import java.util.Locale;
23+
24+
import com.samskivert.mustache.Mustache;
25+
import com.samskivert.mustache.Template.Fragment;
26+
27+
/**
28+
* Splits long fragments into smaller strings and uses a StringBuilder to merge
29+
* them back.
30+
*
31+
* Register:
32+
*
33+
* <pre>
34+
* additionalProperties.put("lambdaSplitString", new SplitStringLambda());
35+
* </pre>
36+
*
37+
* Use:
38+
*
39+
* <pre>
40+
* {{#lambdaSplitString}}{{summary}}{{/lambdaSplitString}}
41+
* </pre>
42+
*/
43+
public class SplitStringLambda implements Mustache.Lambda {
44+
private static final int DEFAULT_MAX_LENGTH = 65535;
45+
46+
private static final String SPLIT_INIT = "new StringBuilder(%d)";
47+
48+
private static final String SPLIT_PART = ".append(\"%s\")";
49+
50+
private static final String SPLIT_SUFFIX = ".toString()";
51+
52+
private final int maxLength;
53+
54+
public SplitStringLambda() {
55+
this(DEFAULT_MAX_LENGTH);
56+
}
57+
58+
public SplitStringLambda(int maxLength) {
59+
this.maxLength = maxLength;
60+
}
61+
62+
@Override
63+
public void execute(Fragment fragment, Writer writer) throws IOException {
64+
String input = fragment.execute();
65+
int inputLength = input.length();
66+
67+
StringBuilder builder = new StringBuilder();
68+
if (inputLength > maxLength) {
69+
70+
// Initialize a StringBuilder
71+
builder.append(String.format(Locale.ROOT, SPLIT_INIT, inputLength));
72+
73+
int currentPosition = 0;
74+
int currentStringLength = 0;
75+
char currentLastChar = '\\';
76+
77+
// Split input into parts of at most maxLength and not ending with an escape character
78+
// Append each part to the StringBuilder
79+
while (currentPosition + maxLength < input.length()) {
80+
currentStringLength = maxLength;
81+
currentLastChar = input.charAt(currentPosition + currentStringLength - 1);
82+
if (currentLastChar == '\\') {
83+
--currentStringLength;
84+
}
85+
86+
builder.append(String.format(Locale.ROOT, SPLIT_PART, input.substring(currentPosition, currentPosition + currentStringLength)));
87+
currentPosition += currentStringLength;
88+
}
89+
90+
// Append last part if necessary
91+
if (currentPosition < input.length()) {
92+
builder.append(String.format(Locale.ROOT, SPLIT_PART, input.substring(currentPosition)));
93+
}
94+
95+
// Close the builder and merge everything back to a string
96+
builder.append(SPLIT_SUFFIX);
97+
} else {
98+
builder.append(String.format(Locale.ROOT, "\"%s\"", input));
99+
}
100+
101+
writer.write(builder.toString());
102+
}
103+
104+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
3+
* Copyright 2018 SmartBear Software
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.openapitools.codegen.templating.mustache;
19+
20+
import java.io.IOException;
21+
import java.io.Writer;
22+
23+
import com.samskivert.mustache.Mustache;
24+
import com.samskivert.mustache.Template.Fragment;
25+
26+
/**
27+
* Replaces duplicate whitespace characters in a fragment with single space.
28+
*
29+
* Register:
30+
* <pre>
31+
* additionalProperties.put("lambdaTrimWhitespace", new TrimWhitespaceLambda());
32+
* </pre>
33+
*
34+
* Use:
35+
* <pre>
36+
* {{#lambdaTrimWhitespace}}{{name}}{{/lambdaTrimWhitespace}}
37+
* </pre>
38+
*/
39+
public class TrimWhitespaceLambda implements Mustache.Lambda {
40+
private static final String SINGLE_SPACE = " ";
41+
42+
private static final String WHITESPACE_REGEX = "\\s+";
43+
44+
@Override
45+
public void execute(Fragment fragment, Writer writer) throws IOException {
46+
writer.write(fragment.execute().replaceAll(WHITESPACE_REGEX, SINGLE_SPACE));
47+
}
48+
49+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{#lambdaSplitString}}{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{#lambdaTrimWhitespace}}{{{example}}}{{/lambdaTrimWhitespace}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}{{/lambdaSplitString}}

modules/openapi-generator/src/main/resources/JavaSpring/methodBody.mustache

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ return CompletableFuture.supplyAsync(()-> {
88
{{#async}} {{/async}} {{/jdk8}}for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
99
{{/-first}}
1010
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) {
11-
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} ApiUtil.setExampleResponse(request, "{{{contentType}}}", "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{example}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}");
11+
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} String exampleString = {{>exampleString}};
12+
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} ApiUtil.setExampleResponse(request, "{{{contentType}}}", exampleString);
1213
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} break;
1314
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} }
1415
{{#-last}}
@@ -36,7 +37,8 @@ Mono<Void> result = Mono.empty();
3637
for (MediaType mediaType : exchange.getRequest().getHeaders().getAccept()) {
3738
{{/-first}}
3839
if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) {
39-
result = ApiUtil.getExampleResponse(exchange, "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{example}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}");
40+
String exampleString = {{>exampleString}};
41+
result = ApiUtil.getExampleResponse(exchange, exampleString);
4042
break;
4143
}
4244
{{#-last}}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
3+
* Copyright 2018 SmartBear Software
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.openapitools.codegen.templating.mustache;
19+
20+
import static org.mockito.Mockito.when;
21+
import static org.testng.Assert.assertEquals;
22+
23+
import java.io.IOException;
24+
import java.io.StringWriter;
25+
import java.util.HashMap;
26+
import java.util.Locale;
27+
import java.util.Map;
28+
29+
import org.mockito.Mock;
30+
import org.mockito.Mockito;
31+
import org.mockito.MockitoAnnotations;
32+
import org.testng.annotations.AfterMethod;
33+
import org.testng.annotations.BeforeMethod;
34+
import org.testng.annotations.Test;
35+
36+
import com.samskivert.mustache.Template.Fragment;
37+
38+
public class SplitStringLambdaTest {
39+
private static final String INPUT_STRING = "1112223334";
40+
41+
private static final Map<Integer, String> EXPECTED_OUTPUTS;
42+
static {
43+
EXPECTED_OUTPUTS = new HashMap<>();
44+
EXPECTED_OUTPUTS.put(2,
45+
String.format(
46+
Locale.ROOT,
47+
"new StringBuilder(%d).append(\"11\").append(\"12\").append(\"22\").append(\"33\").append(\"34\").toString()",
48+
INPUT_STRING.length()));
49+
EXPECTED_OUTPUTS.put(3,
50+
String.format(
51+
Locale.ROOT,
52+
"new StringBuilder(%d).append(\"111\").append(\"222\").append(\"333\").append(\"4\").toString()",
53+
INPUT_STRING.length()));
54+
}
55+
56+
private static final String INPUT_QUOTED_STRING = "1\\\"11\\\"2223\\\"334";
57+
private static final String INPUT_QUOTED_OUTPUT = String.format(
58+
Locale.ROOT,
59+
"new StringBuilder(%d).append(\"1\\\"\").append(\"11\").append(\"\\\"2\").append(\"223\").append(\"\\\"3\").append(\"34\").toString()",
60+
INPUT_QUOTED_STRING.length());
61+
62+
@Mock
63+
private Fragment fragment;
64+
65+
@BeforeMethod
66+
public void init() {
67+
MockitoAnnotations.initMocks(this);
68+
}
69+
70+
@AfterMethod
71+
public void reset() {
72+
Mockito.reset(fragment);
73+
}
74+
75+
private void testString(String input, int maxLength, String expected) throws IOException {
76+
when(fragment.execute()).thenReturn(input);
77+
78+
StringWriter output = new StringWriter();
79+
new SplitStringLambda(maxLength).execute(fragment, output);
80+
assertEquals(output.toString(), expected);
81+
}
82+
83+
@Test
84+
public void testSplitGroupsOf2() throws IOException {
85+
int maxLength = 2;
86+
testString(INPUT_STRING, maxLength, EXPECTED_OUTPUTS.get(maxLength));
87+
}
88+
89+
@Test
90+
public void testSplitGroupsOf3() throws IOException {
91+
int maxLength = 3;
92+
testString(INPUT_STRING, maxLength, EXPECTED_OUTPUTS.get(maxLength));
93+
}
94+
95+
@Test
96+
public void testSplitQuotedString() throws IOException {
97+
int maxLength = 3;
98+
testString(INPUT_QUOTED_STRING, maxLength, INPUT_QUOTED_OUTPUT);
99+
}
100+
101+
@Test
102+
public void testShortString() throws IOException {
103+
testString(INPUT_STRING, INPUT_STRING.length(), String.format(Locale.ROOT, "\"%s\"", INPUT_STRING));
104+
}
105+
106+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
3+
* Copyright 2018 SmartBear Software
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.openapitools.codegen.templating.mustache;
19+
20+
import static org.mockito.Mockito.when;
21+
import static org.testng.Assert.assertEquals;
22+
23+
import java.io.IOException;
24+
import java.io.StringWriter;
25+
26+
import org.mockito.Mock;
27+
import org.mockito.Mockito;
28+
import org.mockito.MockitoAnnotations;
29+
import org.testng.annotations.AfterMethod;
30+
import org.testng.annotations.BeforeMethod;
31+
import org.testng.annotations.Test;
32+
33+
import com.samskivert.mustache.Template.Fragment;
34+
35+
public class TrimWhitespaceLambdaTest {
36+
37+
@Mock
38+
private Fragment fragment;
39+
40+
@BeforeMethod
41+
public void init() {
42+
MockitoAnnotations.initMocks(this);
43+
}
44+
45+
@AfterMethod
46+
public void reset() {
47+
Mockito.reset(fragment);
48+
}
49+
50+
@Test
51+
public void testTrimWhitespace() throws IOException {
52+
when(fragment.execute()).thenReturn("\t a b\t\tc \t");
53+
54+
StringWriter output = new StringWriter();
55+
new TrimWhitespaceLambda().execute(fragment, output);
56+
assertEquals(output.toString(), " a b c ");
57+
}
58+
59+
}

0 commit comments

Comments
 (0)