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);