diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 73829f24..56f1fd4c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -31,6 +31,7 @@ import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.Base64Utils; @@ -165,9 +166,41 @@ public class CurlRequestSnippet extends TemplatedSnippet { } } else if (isPutOrPost(request)) { - String queryString = request.getParameters().toQueryString(); - if (StringUtils.hasText(queryString)) { - writer.print(String.format(" -d '%s'", queryString)); + writeContentUsingParameters(request, writer); + } + } + + private void writeContentUsingParameters(OperationRequest request, PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + String queryString = uniqueParameters.toQueryString(); + if (StringUtils.hasText(queryString)) { + writer.print(String.format(" -d '%s'", queryString)); + } + } + + private Parameters getUniqueParameters(OperationRequest request) { + Parameters queryStringParameters = new QueryStringParser() + .parse(request.getUri()); + Parameters uniqueParameters = new Parameters(); + + for (Entry> parameter : request.getParameters().entrySet()) { + addIfUnique(parameter, queryStringParameters, uniqueParameters); + } + return uniqueParameters; + } + + private void addIfUnique(Entry> parameter, + Parameters queryStringParameters, Parameters uniqueParameters) { + if (!queryStringParameters.containsKey(parameter.getKey())) { + uniqueParameters.put(parameter.getKey(), parameter.getValue()); + } + else { + List candidates = parameter.getValue(); + List existing = queryStringParameters.get(parameter.getKey()); + for (String candidate : candidates) { + if (!existing.contains(candidate)) { + uniqueParameters.add(parameter.getKey(), candidate); + } } } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java new file mode 100644 index 00000000..d09d6e44 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java @@ -0,0 +1,83 @@ +/* + * Copyright 2014-2015 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.curl; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Scanner; + +import org.springframework.restdocs.operation.Parameters; + +/** + * A parser for the query string of a URI. + * + * @author Andy Wilkinson + */ +public class QueryStringParser { + + /** + * Parses the query string of the given {@code uri} and returns the resulting + * {@link Parameters}. + * + * @param uri the uri to parse + * @return the parameters parsed from the query string + */ + public Parameters parse(URI uri) { + String query = uri.getRawQuery(); + if (query != null) { + return parse(query); + } + return new Parameters(); + } + + private Parameters parse(String query) { + Parameters parameters = new Parameters(); + try (Scanner scanner = new Scanner(query)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private void processParameter(String parameter, Parameters parameters) { + String[] components = parameter.split("="); + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + throw new IllegalArgumentException("The parameter '" + parameter + + "' is malformed"); + } + } + + private String decode(String encoded) { + try { + return URLDecoder.decode(encoded, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("Unable to URL encode " + encoded + + " using UTF-8", ex); + } + + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index f84cf0e1..21c1973d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -84,7 +84,7 @@ public class CurlRequestSnippetTests { } @Test - public void requestWithQueryString() throws IOException { + public void getRequestWithQueryString() throws IOException { this.snippet.expectCurlRequest("request-with-query-string") .withContents( codeBlock("bash").content( @@ -94,6 +94,16 @@ public class CurlRequestSnippetTests { "http://localhost/foo?param=value").build()); } + @Test + public void postRequestWithQueryString() throws IOException { + this.snippet.expectCurlRequest("post-request-with-query-string").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo?param=value' -i -X POST")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-query-string", this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param=value").method("POST").build()); + } + @Test public void postRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( @@ -132,6 +142,35 @@ public class CurlRequestSnippetTests { .method("POST").param("k1", "a&b").build()); } + @Test + public void postRequestWithQueryStringAndParameter() throws IOException { + this.snippet + .expectCurlRequest("post-request-with-query-string-and-parameter") + .withContents( + codeBlock("bash") + .content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-query-string-and-parameter", this.snippet + .getOutputDirectory()).request("http://localhost/foo?a=alpha") + .method("POST").param("b", "bravo").build()); + } + + @Test + public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectCurlRequest( + "post-request-with-overlapping-query-string-and-parameters") + .withContents( + codeBlock("bash") + .content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-overlapping-query-string-and-parameters", this.snippet + .getOutputDirectory()).request("http://localhost/foo?a=alpha") + .method("POST").param("a", "alpha").param("b", "bravo").build()); + } + @Test public void putRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java new file mode 100644 index 00000000..bfdc7010 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2015 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.curl; + +import java.net.URI; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.operation.Parameters; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link QueryStringParser}. + * + * @author Andy Wilkinson + */ +public class QueryStringParserTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final QueryStringParser queryStringParser = new QueryStringParser(); + + @Test + public void noParameters() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost")); + assertThat(parameters.size(), is(equalTo(0))); + } + + @Test + public void singleParameter() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=alpha")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); + } + + @Test + public void multipleParameters() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=alpha&b=bravo&c=charlie")); + assertThat(parameters.size(), is(equalTo(3))); + assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); + assertThat(parameters, hasEntry("b", Arrays.asList("bravo"))); + assertThat(parameters, hasEntry("c", Arrays.asList("charlie"))); + } + + @Test + public void multipleParametersWithSameKey() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=apple&a=avocado")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("apple", "avocado"))); + } + + @Test + public void encoded() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=al%26%3Dpha")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("al&=pha"))); + } + + @Test + public void malformedParameter() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown + .expectMessage(equalTo("The parameter 'a=apple=avocado' is malformed")); + this.queryStringParser.parse(URI.create("http://localhost?a=apple=avocado")); + } +} diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index a7422f94..cbf72544 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -136,6 +136,25 @@ public class MockMvcRestDocumentationIntegrationTests { + "-H 'Accept: application/json' -d 'content'")))); } + @Test + public void curlSnippetWithQueryStringOnPost() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform( + post("/?foo=bar").param("foo", "bar").param("a", "alpha") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("curl-snippet-with-query-string")); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), + is(snippet().withContents( + codeBlock("bash").content( + "$ curl " + + "'http://localhost:8080/?foo=bar' -i -X POST " + + "-H 'Accept: application/json' -d 'a=alpha'")))); + } + @Test public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)