From 404b99b0a4ea4dc1908b5d7d709db9e6e5a1f2b2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 10:50:32 +0100 Subject: [PATCH] Avoid problems caused by | characters in table cell content Previously, a | character in a cell of an Asciidoctor table would break that table's formatting as it would be interpretted as a delimiter for the cell. The problem also affects Markdown tables where the | character is not within backticks. This commit addresses the problem with Asciidoctor tables by introducing a JMustache Lambda that escapes | characters by prefixing them with a backslash. Unfortunately, a complete solution to the problem when using Markdown is not as straightforward. Markdown only requires a | character to be espaced when it is not within backticks. Determing this isn't straightforward as the text would have to be parsed, taking into account the possibility of backticks themselves being escaped. Instead, this commit attempts to avoid the problem in a different way. The most likely source of a | character is when documenting a `@Pattern` constraint that uses a | character in its regular expression. To improve the readability of the regular expression this commit wraps it in backticks, thereby formatting it in a monospaced font in both Asciidoctor and Markdown and also avoiding any escaping problems with the latter. Closes gh-232 See gh-230 --- spring-restdocs-core/build.gradle | 1 + .../config/RestDocumentationConfigurer.java | 13 ++++- .../AsciidoctorTableCellContentLambda.java | 45 +++++++++++++++ .../templates/mustache/MustacheTemplate.java | 23 +++++++- .../mustache/MustacheTemplateEngine.java | 30 +++++++++- .../DefaultConstraintDescriptions.properties | 2 +- .../asciidoctor/default-links.snippet | 4 +- .../default-path-parameters.snippet | 4 +- .../default-request-fields.snippet | 6 +- .../default-request-headers.snippet | 4 +- .../default-request-parameters.snippet | 4 +- .../asciidoctor/default-request-parts.snippet | 4 +- .../default-response-fields.snippet | 6 +- .../default-response-headers.snippet | 4 +- .../RestDocumentationConfigurerTests.java | 30 ++++++++++ .../ConstraintDescriptionsTests.java | 2 +- ...dleConstraintDescriptionResolverTests.java | 2 +- .../headers/RequestHeadersSnippetTests.java | 20 +++++++ .../headers/ResponseHeadersSnippetTests.java | 19 +++++++ .../hypermedia/LinksSnippetTests.java | 19 +++++++ .../payload/RequestFieldsSnippetTests.java | 22 +++++++ .../payload/ResponseFieldsSnippetTests.java | 21 +++++++ .../request/PathParametersSnippetTests.java | 32 ++++++++++- .../RequestParametersSnippetTests.java | 20 +++++++ .../request/RequestPartsSnippetTests.java | 20 +++++++ ...sciidoctorTableCellContentLambdaTests.java | 57 +++++++++++++++++++ .../restdocs/test/OperationBuilder.java | 8 ++- 27 files changed, 393 insertions(+), 29 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 9f093475..01ef4b9c 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -38,6 +38,7 @@ dependencies { testCompile 'org.hamcrest:hamcrest-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.hibernate:hibernate-validator' + testCompile 'org.springframework:spring-test' testRuntime 'org.glassfish:javax.el:3.0.0' } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index f77c7aa4..2b1216f4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -17,15 +17,19 @@ package org.springframework.restdocs.config; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.mustache.Mustache; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; /** @@ -102,9 +106,16 @@ public abstract class RestDocumentationConfigurer templateContext = new HashMap<>(); + if (snippetConfiguration.getTemplateFormat().getId() + .equals(TemplateFormats.asciidoctor().getId())) { + templateContext.put("tableCellContent", + new AsciidoctorTableCellContentLambda()); + } engineToUse = new MustacheTemplateEngine( new StandardTemplateResourceResolver( - snippetConfiguration.getTemplateFormat())); + snippetConfiguration.getTemplateFormat()), + Mustache.compiler().escapeHTML(false), templateContext); } configuration.put(TemplateEngine.class.getName(), engineToUse); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java new file mode 100644 index 00000000..7881c6e0 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.templates.mustache; + +import java.io.IOException; +import java.io.Writer; + +import org.springframework.restdocs.mustache.Mustache.Lambda; +import org.springframework.restdocs.mustache.Template.Fragment; + +/** + * A {@link Lambda} that escapes {@code |} characters so that the do not break the table's + * formatting. + * + * @author Andy Wilkinson + */ +public final class AsciidoctorTableCellContentLambda implements Lambda { + + @Override + public void execute(Fragment fragment, Writer writer) throws IOException { + String output = fragment.execute(); + for (int i = 0; i < output.length(); i++) { + char current = output.charAt(i); + if (current == '|' && (i == 0 || output.charAt(i - 1) != '\\')) { + writer.append('\\'); + } + writer.append(current); + } + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java index cb98ab2a..1d315360 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.templates.mustache; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.springframework.restdocs.templates.Template; @@ -30,18 +32,37 @@ public class MustacheTemplate implements Template { private final org.springframework.restdocs.mustache.Template delegate; + private final Map context; + /** * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. * * @param delegate The delegate to adapt */ public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate) { + this(delegate, Collections.emptyMap()); + } + + /** + * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. + * During rendering, the given {@code context} and the context passed into + * {@link #render(Map)} will be combined and then passed to the delegate when it is + * {@link org.springframework.restdocs.mustache.Template#execute executed}. + * + * @param delegate The delegate to adapt + * @param context The context + */ + public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate, + Map context) { this.delegate = delegate; + this.context = context; } @Override public String render(Map context) { - return this.delegate.execute(context); + Map combinedContext = new HashMap<>(this.context); + combinedContext.putAll(context); + return this.delegate.execute(combinedContext); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index 8079060a..fadce5ae 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -18,6 +18,8 @@ package org.springframework.restdocs.templates.mustache; import java.io.IOException; import java.io.InputStreamReader; +import java.util.Collections; +import java.util.Map; import org.springframework.core.io.Resource; import org.springframework.restdocs.mustache.Mustache; @@ -36,9 +38,11 @@ import org.springframework.restdocs.templates.TemplateResourceResolver; */ public class MustacheTemplateEngine implements TemplateEngine { + private final TemplateResourceResolver templateResourceResolver; + private final Compiler compiler; - private final TemplateResourceResolver templateResourceResolver; + private final Map context; /** * Creates a new {@code MustacheTemplateEngine} that will use the given @@ -60,16 +64,36 @@ public class MustacheTemplateEngine implements TemplateEngine { */ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, Compiler compiler) { + this(templateResourceResolver, compiler, Collections.emptyMap()); + } + + /** + * Creates a new {@code MustacheTemplateEngine} that will use the given + * {@code templateResourceResolver} to resolve templates and the given + * {@code compiler} to compile them. Compiled templates will be created with the given + * {@code context}. + * + * @param templateResourceResolver the resolver to use + * @param compiler the compiler to use + * @param context the context to pass to compiled templates + * @see MustacheTemplate#MustacheTemplate(org.springframework.restdocs.mustache.Template, + * Map) + */ + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, + Compiler compiler, Map context) { this.templateResourceResolver = templateResourceResolver; this.compiler = compiler; + this.context = context; } @Override public Template compileTemplate(String name) throws IOException { Resource templateResource = this.templateResourceResolver .resolveTemplateResource(name); - return new MustacheTemplate(this.compiler - .compile(new InputStreamReader(templateResource.getInputStream()))); + return new MustacheTemplate( + this.compiler.compile( + new InputStreamReader(templateResource.getInputStream())), + this.context); } /** diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties index 1cd5320f..d42b5ae6 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties @@ -9,7 +9,7 @@ javax.validation.constraints.Min.description=Must be at least ${value} javax.validation.constraints.NotNull.description=Must not be null javax.validation.constraints.Null.description=Must be null javax.validation.constraints.Past.description=Must be in the past -javax.validation.constraints.Pattern.description=Must match the regular expression '${regexp}' +javax.validation.constraints.Pattern.description=Must match the regular expression `${regexp}` javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive org.hibernate.validator.constraints.CreditCardNumber.description=Must be a well-formed credit card number org.hibernate.validator.constraints.EAN.description=Must be a well-formed ${type} number diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet index 132643d8..ab50823d 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet @@ -2,8 +2,8 @@ |Relation|Description {{#links}} -|`{{rel}}` -|{{description}} +|{{#tableCellContent}}`{{rel}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/links}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet index a6cc9f8d..c318e401 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet @@ -3,8 +3,8 @@ |Parameter|Description {{#parameters}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/parameters}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet index 41e763e7..46cd43fe 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet @@ -2,9 +2,9 @@ |Path|Type|Description {{#fields}} -|`{{path}}` -|`{{type}}` -|{{description}} +|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} +|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet index f3d66035..790a81bc 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet @@ -2,8 +2,8 @@ |Name|Description {{#headers}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/headers}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet index f338b345..411c33c5 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet @@ -2,8 +2,8 @@ |Parameter|Description {{#parameters}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/parameters}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet index 3ed4773b..06a65c1e 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet @@ -2,8 +2,8 @@ |Part|Description {{#requestParts}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/requestParts}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet index 41e763e7..46cd43fe 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet @@ -2,9 +2,9 @@ |Path|Type|Description {{#fields}} -|`{{path}}` -|`{{type}}` -|{{description}} +|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} +|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet index f3d66035..790a81bc 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet @@ -2,8 +2,8 @@ |Name|Description {{#headers}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/headers}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 0872c20d..995a6ffb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -36,7 +36,9 @@ import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.test.util.ReflectionTestUtils; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; @@ -164,6 +166,34 @@ public class RestDocumentationConfigurerTests { is(equalTo(TemplateFormats.markdown()))); } + @SuppressWarnings("unchecked") + @Test + public void asciidoctorTableCellContentLambaIsInstalledWhenUsingAsciidoctorTemplateFormat() { + Map configuration = new HashMap<>(); + this.configurer.apply(configuration, createContext()); + TemplateEngine templateEngine = (TemplateEngine) configuration + .get(TemplateEngine.class.getName()); + MustacheTemplateEngine mustacheTemplateEngine = (MustacheTemplateEngine) templateEngine; + Map templateContext = (Map) ReflectionTestUtils + .getField(mustacheTemplateEngine, "context"); + assertThat(templateContext, hasEntry(equalTo("tableCellContent"), + instanceOf(AsciidoctorTableCellContentLambda.class))); + } + + @SuppressWarnings("unchecked") + @Test + public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidoctorTemplateFormat() { + Map configuration = new HashMap<>(); + this.configurer.snippetConfigurer.withTemplateFormat(TemplateFormats.markdown()); + this.configurer.apply(configuration, createContext()); + TemplateEngine templateEngine = (TemplateEngine) configuration + .get(TemplateEngine.class.getName()); + MustacheTemplateEngine mustacheTemplateEngine = (MustacheTemplateEngine) templateEngine; + Map templateContext = (Map) ReflectionTestUtils + .getField(mustacheTemplateEngine, "context"); + assertThat(templateContext.size(), equalTo(0)); + } + private RestDocumentationContext createContext() { ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( "build"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index a3dbd9ab..34e916c5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -118,7 +118,7 @@ public class ConstraintDescriptionsTests { @Test public void pattern() { assertThat(this.constraintDescriptions.descriptionsForProperty("pattern"), - contains("Must match the regular expression '[A-Z][a-z]+'")); + contains("Must match the regular expression `[A-Z][a-z]+`")); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index f9a7f734..ece62ab9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -137,7 +137,7 @@ public class ResourceBundleConstraintDescriptionResolverTests { @Test public void defaultMessagePattern() { assertThat(constraintDescriptionForField("pattern"), - is(equalTo("Must match the regular expression '[A-Z][a-z]+'"))); + is(equalTo("Must match the regular expression `[A-Z][a-z]+`"))); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 7a0c9edd..5766661c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.OperationBuilder; @@ -165,4 +166,23 @@ public class RequestHeadersSnippetTests extends AbstractSnippetTests { .header("Connection", "keep-alive").build()); } + @Test + public void tableCellContentIsEscapedWhenNecessary() throws IOException { + this.snippet.expectRequestHeaders("request-with-escaped-headers").withContents( + tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) + .document(operationBuilder("request-with-escaped-headers") + .request("http://localhost").header("Foo|Bar", "baz") + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index d7bf553a..417259cb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.OperationBuilder; @@ -155,4 +156,22 @@ public class ResponseHeadersSnippetTests extends AbstractSnippetTests { .build()); } + @Test + public void tableCellContentIsEscapedWhenNecessary() throws IOException { + this.snippet.expectResponseHeaders("response-with-escaped-headers").withContents( + tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) + .document(operationBuilder("response-with-escaped-headers") + .response().header("Foo|Bar", "baz").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 46f73b30..eb14248b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -171,4 +172,22 @@ public class LinksSnippetTests extends AbstractSnippetTests { .and(new LinkDescriptor("b").description("two")) .document(operationBuilder("additional-descriptors").build()); } + + @Test + public void tableCellContentIsEscapedWhenNecessary() throws IOException { + this.snippet.expectLinks("links-with-escaped-content") + .withContents(tableWithHeader("Relation", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("Foo|Bar", "foo")), + Arrays.asList(new LinkDescriptor("Foo|Bar").description("one|two"))) + .document(operationBuilder("links-with-escaped-content").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index eac0b843..fa23f346 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -26,6 +26,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -227,4 +228,25 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests { .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } + @Test + public void requestWithFieldsWithEscapedContent() throws IOException { + this.snippet.expectRequestFields("request-fields-with-escaped-content") + .withContents(tableWithHeader("Path", "Type", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), + escapeIfNecessary("three|four"))); + + new RequestFieldsSnippet(Arrays.asList( + fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) + .document(operationBuilder("request-fields-with-escaped-content") + .request("http://localhost").content("{\"Foo|Bar\": 5}") + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 574899a0..db13d942 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -26,6 +26,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -283,4 +284,24 @@ public class ResponseFieldsSnippetTests extends AbstractSnippetTests { .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } + @Test + public void responseWithFieldsWithEscapedContent() throws IOException { + this.snippet.expectResponseFields("response-fields-with-escaped-content") + .withContents(tableWithHeader("Path", "Type", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), + escapeIfNecessary("three|four"))); + + new ResponseFieldsSnippet(Arrays.asList( + fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) + .document(operationBuilder("response-fields-with-escaped-content") + .response().content("{\"Foo|Bar\": 5}").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index c5f70065..e0980991 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -192,9 +192,37 @@ public class PathParametersSnippetTests extends AbstractSnippetTests { .build()); } + @Test + public void pathParametersWithEscapedContent() throws IOException { + this.snippet.expectPathParameters("path-parameters-with-escaped-content") + .withContents(tableWithTitleAndHeader(getTitle("{Foo|Bar}"), "Parameter", + "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); + + RequestDocumentation + .pathParameters(parameterWithName("Foo|Bar").description("one|two")) + .document(operationBuilder("path-parameters-with-escaped-content") + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "{Foo|Bar}") + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + private String getTitle() { - return this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}/{b}" - : "`/{a}/{b}`"; + return getTitle("/{a}/{b}"); + } + + private String getTitle(String title) { + if (this.templateFormat.equals(TemplateFormats.asciidoctor())) { + return title; + } + return "`" + title + "`"; } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 94135a3b..78dac57d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -194,4 +195,23 @@ public class RequestParametersSnippetTests extends AbstractSnippetTests { .param("b", "bravo").build()); } + @Test + public void requestParametersWithEscapedContent() throws IOException { + this.snippet.expectRequestParameters("request-parameters-with-escaped-content") + .withContents(tableWithHeader("Parameter", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + + RequestDocumentation + .requestParameters(parameterWithName("Foo|Bar").description("one|two")) + .document(operationBuilder("request-parameters-with-escaped-content") + .request("http://localhost").param("Foo|Bar", "baz").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java index 6ce2a09a..6468d80b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -180,4 +181,23 @@ public class RequestPartsSnippetTests extends AbstractSnippetTests { .part("b", "bravo".getBytes()).build()); } + @Test + public void requestPartsWithEscapedContent() throws IOException { + this.snippet.expectRequestParts("request-parts-with-escaped-content") + .withContents(tableWithHeader("Part", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + + RequestDocumentation.requestParts(partWithName("Foo|Bar").description("one|two")) + .document(operationBuilder("request-parts-with-escaped-content") + .request("http://localhost").part("Foo|Bar", "baz".getBytes()) + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java new file mode 100644 index 00000000..c9420919 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.templates.mustache; + +import java.io.IOException; +import java.io.StringWriter; + +import org.junit.Test; + +import org.springframework.restdocs.mustache.Template.Fragment; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AsciidoctorTableCellContentLambda}. + * + * @author Andy Wilkinson + */ +public class AsciidoctorTableCellContentLambdaTests { + + @Test + public void verticalBarCharactersAreEscaped() throws IOException { + Fragment fragment = mock(Fragment.class); + given(fragment.execute()).willReturn("|foo|bar|baz|"); + StringWriter writer = new StringWriter(); + new AsciidoctorTableCellContentLambda().execute(fragment, writer); + assertThat(writer.toString(), is(equalTo("\\|foo\\|bar\\|baz\\|"))); + } + + @Test + public void escapedVerticalBarCharactersAreNotEscapedAgain() throws IOException { + Fragment fragment = mock(Fragment.class); + given(fragment.execute()).willReturn("\\|foo|bar\\|baz|"); + StringWriter writer = new StringWriter(); + new AsciidoctorTableCellContentLambda().execute(fragment, writer); + assertThat(writer.toString(), is(equalTo("\\|foo\\|bar\\|baz\\|"))); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 86b426a3..4e292b2e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -29,6 +29,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.mustache.Mustache; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestFactory; @@ -45,6 +46,7 @@ import org.springframework.restdocs.templates.StandardTemplateResourceResolver; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; /** @@ -93,9 +95,13 @@ public class OperationBuilder { public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { + Map templateContext = new HashMap<>(); + templateContext.put("tableCellContent", + new AsciidoctorTableCellContentLambda()); this.attributes.put(TemplateEngine.class.getName(), new MustacheTemplateEngine( - new StandardTemplateResourceResolver(this.templateFormat))); + new StandardTemplateResourceResolver(this.templateFormat), + Mustache.compiler().escapeHTML(false), templateContext)); } RestDocumentationContext context = createContext(); this.attributes.put(RestDocumentationContext.class.getName(), context);