Isolate and reduce Spring Test dependencies

This commit splits Spring REST Docs into two projects –
spring-restdocs-core and spring-restdocs-mockmvc.

spring-restdocs-core contains the vast majority of the code but does not
depend on a specific test framework other than JUnit. The use of a
Spring Test TestExecutionListener has been replaced with a JUnit test
rule. The rule is declared once per test class and configured with
the output directory to which the generated snippets should be written.
This simplifies the implementation as thread local storage is no longer
required to transfer information about the test that’s running into
Spring REST Docs. Instead, this transfer is now handled by the new test
rule. It has also simplified the configuration as it’s no longer
necessary for users to provide a system property that configures the
output directory.

spring-restdocs-mockmvc contains code that’s specific to using Spring
REST Docs with Spring MVC Test’s MockMvc. This is currently the only
testing framework that’s supported, but it paves the way for adding
support for additional frameworks. REST Assured is one that users seem
particularly interested in (see gh-80 and gh-102).

Closes gh-107
This commit is contained in:
Andy Wilkinson
2015-09-02 16:20:34 +01:00
parent 9d8bbf0558
commit 2b2b6fcd25
207 changed files with 1396 additions and 1425 deletions

View File

@@ -0,0 +1,81 @@
/*
* 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;
import java.io.File;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
/**
* A JUnit {@link TestRule} used to bootstrap the generation of REST documentation from
* JUnit tests.
*
* @author Andy Wilkinson
*
*/
public class RestDocumentation implements TestRule {
private final String outputDirectory;
private RestDocumentationContext context;
/**
* Creates a new {@code RestDocumentation} instance that will generate snippets to the
* given {@code outputDirectory}
*
* @param outputDirectory the output directory
*/
public RestDocumentation(String outputDirectory) {
this.outputDirectory = outputDirectory;
}
@Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Class<?> testClass = description.getTestClass();
String methodName = description.getMethodName();
RestDocumentation.this.context = new RestDocumentationContext(testClass,
methodName, new File(RestDocumentation.this.outputDirectory));
try {
base.evaluate();
}
finally {
RestDocumentation.this.context = null;
}
}
};
}
/**
* Notification that a RESTful operation that should be documented is about to be
* performed. Returns a {@link RestDocumentationContext} for the operation.
*
* @return the context for the operation
*/
public RestDocumentationContext beforeOperation() {
this.context.getAndIncrementStepCount();
return this.context;
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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;
import java.io.File;
import java.util.concurrent.atomic.AtomicInteger;
/**
* {@code RestDocumentationContext} encapsulates the context in which the documentation of
* a RESTful API is being performed.
*
* @author Andy Wilkinson
*/
public final class RestDocumentationContext {
private final AtomicInteger stepCount = new AtomicInteger(0);
private final Class<?> testClass;
private final String testMethodName;
private final File outputDirectory;
/**
* Creates a new {@code RestDocumentationContext} for a test on the given
* {@code testClass} with given {@code testMethodName} that will generate
* documentation to the given {@code outputDirectory}.
*
* @param testClass the class whose test is being executed
* @param testMethodName the name of the test method that is being executed
* @param outputDirectory the directory to which documentation should be written.
*/
public RestDocumentationContext(Class<?> testClass, String testMethodName,
File outputDirectory) {
this.testClass = testClass;
this.testMethodName = testMethodName;
this.outputDirectory = outputDirectory;
}
/**
* Returns the class whose tests are currently executing
*
* @return The test class
*/
public Class<?> getTestClass() {
return this.testClass;
}
/**
* Returns the name of the test method that is currently executing
*
* @return The name of the test method
*/
public String getTestMethodName() {
return this.testMethodName;
}
/**
* Returns the current step count and then increments it
*
* @return The step count prior to it being incremented
*/
int getAndIncrementStepCount() {
return this.stepCount.getAndIncrement();
}
/**
* Returns the current step count
*
* @return The current step count
*/
public int getStepCount() {
return this.stepCount.get();
}
/**
* Returns the output directory to which generated snippets should be written.
*
* @return the output directory
*/
public File getOutputDirectory() {
return this.outputDirectory;
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.constraints;
import java.util.Map;
/**
* A constraint
*
* @author Andy Wilkinson
*/
public class Constraint {
private final String name;
private final Map<String, Object> configuration;
/**
* Creates a new {@code Constraint} with the given {@code name} and
* {@code configuration}.
*
* @param name the name
* @param configuration the configuration
*/
public Constraint(String name, Map<String, Object> configuration) {
this.name = name;
this.configuration = configuration;
}
/**
* Returns the name of the constraint
*
* @return the name
*/
public String getName() {
return this.name;
}
/**
* Returns the configuration of the constraint
*
* @return the configuration
*/
public Map<String, Object> getConfiguration() {
return this.configuration;
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.constraints;
/**
* Resolves a description for a {@link Constraint}.
*
* @author Andy Wilkinson
*
*/
public interface ConstraintDescriptionResolver {
/**
* Resolves the description for the given {@code constraint}.
*
* @param constraint the constraint
* @return the description
*/
public String resolveDescription(Constraint constraint);
}

View File

@@ -0,0 +1,108 @@
/*
* 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.constraints;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Provides access to descriptions of a class's constraints
*
* @author Andy Wilkinson
*/
public class ConstraintDescriptions {
private final Class<?> clazz;
private final ConstraintResolver constraintResolver;
private final ConstraintDescriptionResolver descriptionResolver;
/**
* Create a new {@code ConstraintDescriptions} for the given {@code clazz}.
* Constraints will be resolved using a {@link ValidatorConstraintResolver} and
* descriptions will be resolved using a
* {@link ResourceBundleConstraintDescriptionResolver}.
*
* @param clazz the class
*/
public ConstraintDescriptions(Class<?> clazz) {
this(clazz, new ValidatorConstraintResolver(),
new ResourceBundleConstraintDescriptionResolver());
}
/**
* Create a new {@code ConstraintDescriptions} for the given {@code clazz}.
* Constraints will be resolved using the given {@link constraintResolver} and
* descriptions will be resolved using a
* {@link ResourceBundleConstraintDescriptionResolver}.
*
* @param clazz the class
* @param constraintResolver the constraint resolver
*/
public ConstraintDescriptions(Class<?> clazz, ConstraintResolver constraintResolver) {
this(clazz, constraintResolver, new ResourceBundleConstraintDescriptionResolver());
}
/**
* Create a new {@code ConstraintDescriptions} for the given {@code clazz}.
* Constraints will be resolved using a {@link ValidatorConstraintResolver} and
* descriptions will be resolved using the given {@code descriptionResolver}.
*
* @param clazz the class
* @param descriptionResolver the description resolver
*/
public ConstraintDescriptions(Class<?> clazz,
ConstraintDescriptionResolver descriptionResolver) {
this(clazz, new ValidatorConstraintResolver(), descriptionResolver);
}
/**
* Create a new {@code ConstraintDescriptions} for the given {@code clazz}.
* Constraints will be resolved using the given {@code constraintResolver} and
* descriptions will be resolved using the given {@code descriptionResolver}.
*
* @param clazz the class
* @param constraintResolver the constraint resolver
* @param descriptionResolver the description resolver
*/
public ConstraintDescriptions(Class<?> clazz, ConstraintResolver constraintResolver,
ConstraintDescriptionResolver descriptionResolver) {
this.clazz = clazz;
this.constraintResolver = constraintResolver;
this.descriptionResolver = descriptionResolver;
}
/**
* Returns a list of the descriptions for the constraints on the given property
*
* @param property the property
* @return the list of constraint descriptions
*/
public List<String> descriptionsForProperty(String property) {
List<Constraint> constraints = this.constraintResolver.resolveForProperty(
property, this.clazz);
List<String> descriptions = new ArrayList<>();
for (Constraint constraint : constraints) {
descriptions.add(this.descriptionResolver.resolveDescription(constraint));
}
Collections.sort(descriptions);
return descriptions;
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.constraints;
import java.util.List;
/**
* An abstraction for resolving a class's constraints.
*
* @author Andy Wilkinson
*/
public interface ConstraintResolver {
/**
* Resolves and returns the constraints for the given {@code property} on the given
* {@code clazz}. If there are no constraints, an empty list is returned.
*
* @param property the property
* @param clazz the class
* @return the list of constraints, never {@code null}
*/
List<Constraint> resolveForProperty(String property, Class<?> clazz);
}

View File

@@ -0,0 +1,145 @@
/*
* 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.constraints;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
/**
* A {@link ConstraintDescriptionResolver} that resolves constraint descriptions from a
* {@link ResourceBundle}. The resource bundle's keys are the name of the constraint with
* {@code .description} appended. For example, the key for the constraint named
* {@code javax.validation.constraints.NotNull} is
* {@code javax.validation.constraints.NotNull.description}.
* <p>
* Default descriptions are provided for Bean Validation 1.1's constraints:
*
* <ul>
* <li>{@link AssertFalse}
* <li>{@link AssertTrue}
* <li>{@link DecimalMax}
* <li>{@link DecimalMin}
* <li>{@link Digits}
* <li>{@link Future}
* <li>{@link Max}
* <li>{@link Min}
* <li>{@link NotNull}
* <li>{@link Null}
* <li>{@link Past}
* <li>{@link Pattern}
* <li>{@link Size}
* </ul>
*
* @author Andy Wilkinson
*/
public class ResourceBundleConstraintDescriptionResolver implements
ConstraintDescriptionResolver {
private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper(
"${", "}");
private final ResourceBundle defaultDescriptions;
private final ResourceBundle userDescriptions;
/**
* Creates a new {@code ResourceBundleConstraintDescriptionResolver} that will resolve
* descriptions by looking them up in a resource bundle with the base name
* {@code org.springframework.restdocs.constraints.ConstraintDescriptions} in the
* default locale loaded using the thread context class loader
*/
public ResourceBundleConstraintDescriptionResolver() {
this(getBundle("ConstraintDescriptions"));
}
/**
* Creates a new {@code ResourceBundleConstraintDescriptionResolver} that will resolve
* descriptions by looking them up in the given {@code resourceBundle}.
*
* @param resourceBundle the resource bundle
*/
public ResourceBundleConstraintDescriptionResolver(ResourceBundle resourceBundle) {
this.defaultDescriptions = getBundle("DefaultConstraintDescriptions");
this.userDescriptions = resourceBundle;
}
private static ResourceBundle getBundle(String name) {
try {
return ResourceBundle.getBundle(
ResourceBundleConstraintDescriptionResolver.class.getPackage()
.getName() + "." + name, Locale.getDefault(), Thread
.currentThread().getContextClassLoader());
}
catch (MissingResourceException ex) {
return null;
}
}
@Override
public String resolveDescription(Constraint constraint) {
String key = constraint.getName() + ".description";
return this.propertyPlaceholderHelper.replacePlaceholders(getDescription(key),
new ConstraintPlaceholderResolver(constraint));
}
private String getDescription(String key) {
try {
if (this.userDescriptions != null) {
return this.userDescriptions.getString(key);
}
}
catch (MissingResourceException ex) {
// Continue and return default description, if available
}
return this.defaultDescriptions.getString(key);
}
private static class ConstraintPlaceholderResolver implements PlaceholderResolver {
private final Constraint constraint;
private ConstraintPlaceholderResolver(Constraint constraint) {
this.constraint = constraint;
}
@Override
public String resolvePlaceholder(String placeholderName) {
Object replacement = this.constraint.getConfiguration().get(placeholderName);
return replacement != null ? replacement.toString() : null;
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.constraints;
import java.util.ArrayList;
import java.util.List;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotNull;
import javax.validation.metadata.BeanDescriptor;
import javax.validation.metadata.ConstraintDescriptor;
import javax.validation.metadata.PropertyDescriptor;
/**
* A {@link ConstraintResolver} that uses a Bean Validation {@link Validator} to resolve
* constraints. The name of the constraint is the fully-qualified class name of the
* constraint annotation. For example, a {@link NotNull} constraint will be named
* {@code javax.validation.constraints.NotNull}.
*
* @author Andy Wilkinson
*
*/
public class ValidatorConstraintResolver implements ConstraintResolver {
private final Validator validator;
/**
* Creates a new {@code ValidatorConstraintResolver} that will use a {@link Validator}
* in its default configurationto resolve constraints.
*
* @see Validation#buildDefaultValidatorFactory()
* @see ValidatorFactory#getValidator()
*/
public ValidatorConstraintResolver() {
this(Validation.buildDefaultValidatorFactory().getValidator());
}
/**
* Creates a new {@code ValidatorConstraintResolver} that will use the given
* {@code Validator} to resolve constraints.
*
* @param validator the validator
*/
public ValidatorConstraintResolver(Validator validator) {
this.validator = validator;
}
@Override
public List<Constraint> resolveForProperty(String property, Class<?> clazz) {
List<Constraint> constraints = new ArrayList<>();
BeanDescriptor beanDescriptor = this.validator.getConstraintsForClass(clazz);
PropertyDescriptor propertyDescriptor = beanDescriptor
.getConstraintsForProperty(property);
if (propertyDescriptor != null) {
for (ConstraintDescriptor<?> constraintDescriptor : propertyDescriptor
.getConstraintDescriptors()) {
constraints
.add(new Constraint(constraintDescriptor.getAnnotation()
.annotationType().getName(), constraintDescriptor
.getAttributes()));
}
}
return constraints;
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.util.Map;
import org.springframework.restdocs.snippet.Snippet;
/**
* Static factory methods for documenting a RESTful API as if it were being driven using
* the cURL command-line utility.
*
* @author Andy Wilkinson
* @author Yann Le Guern
* @author Dmitriy Mayboroda
* @author Jonathan Pearlin
*/
public abstract class CurlDocumentation {
private CurlDocumentation() {
}
/**
* Returns a handler that will produce a snippet containing the curl request for the
* API call.
*
* @return the handler that will produce the snippet
*/
public static Snippet curlRequest() {
return new CurlRequestSnippet();
}
/**
* Returns a handler that will produce a snippet containing the curl request for the
* API call. The given {@code attributes} will be available during snippet generation.
*
* @param attributes Attributes made available during rendering of the curl request
* snippet
* @return the handler that will produce the snippet
*/
public static Snippet curlRequest(Map<String, Object> attributes) {
return new CurlRequestSnippet(attributes);
}
}

View File

@@ -0,0 +1,136 @@
/*
* 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.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
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.snippet.Snippet;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.util.StringUtils;
/**
* A {@link Snippet} that documents the curl command for a request.
*
* @author Andy Wilkinson
*/
class CurlRequestSnippet extends TemplatedSnippet {
CurlRequestSnippet() {
this(null);
}
CurlRequestSnippet(Map<String, Object> attributes) {
super("curl-request", attributes);
}
@Override
public Map<String, Object> createModel(Operation operation) throws IOException {
Map<String, Object> model = new HashMap<String, Object>();
model.put("arguments", getCurlCommandArguments(operation));
return model;
}
private String getCurlCommandArguments(Operation operation) throws IOException {
StringWriter command = new StringWriter();
PrintWriter printer = new PrintWriter(command);
printer.print("'");
printer.print(operation.getRequest().getUri());
printer.print("'");
writeOptionToIncludeHeadersInOutput(printer);
writeHttpMethodIfNecessary(operation.getRequest(), printer);
writeHeaders(operation.getRequest(), printer);
writePartsIfNecessary(operation.getRequest(), printer);
writeContent(operation.getRequest(), printer);
return command.toString();
}
private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) {
writer.print(" -i");
}
private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) {
if (!HttpMethod.GET.equals(request.getMethod())) {
writer.print(String.format(" -X %s", request.getMethod()));
}
}
private void writeHeaders(OperationRequest request, PrintWriter writer) {
for (Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
for (String header : entry.getValue()) {
writer.print(String.format(" -H '%s: %s'", entry.getKey(), header));
}
}
}
private void writePartsIfNecessary(OperationRequest request, PrintWriter writer)
throws IOException {
for (OperationRequestPart part : request.getParts()) {
writer.printf(" -F '%s=", part.getName());
if (!StringUtils.hasText(part.getSubmittedFileName())) {
writer.append(new String(part.getContent()));
}
else {
writer.printf("@%s", part.getSubmittedFileName());
}
if (part.getHeaders().getContentType() != null) {
writer.append(";type=").append(
part.getHeaders().getContentType().toString());
}
writer.append("'");
}
}
private void writeContent(OperationRequest request, PrintWriter writer)
throws IOException {
if (request.getContent().length > 0) {
writer.print(String.format(" -d '%s'", new String(request.getContent())));
}
else if (!request.getParts().isEmpty()) {
for (Entry<String, List<String>> entry : request.getParameters().entrySet()) {
for (String value : entry.getValue()) {
writer.print(String.format(" -F '%s=%s'", entry.getKey(), value));
}
}
}
else if (isPutOrPost(request)) {
String queryString = request.getParameters().toQueryString();
if (StringUtils.hasText(queryString)) {
writer.print(String.format(" -d '%s'", queryString));
}
}
}
private boolean isPutOrPost(OperationRequest request) {
return HttpMethod.PUT.equals(request.getMethod())
|| HttpMethod.POST.equals(request.getMethod());
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.http;
import java.util.Map;
import org.springframework.restdocs.snippet.Snippet;
/**
* Static factory methods for documenting a RESTful API's HTTP requests.
*
* @author Andy Wilkinson
* @author Jonathan Pearlin
*/
public abstract class HttpDocumentation {
private HttpDocumentation() {
}
/**
* Returns a handler that will produce a snippet containing the HTTP request for the
* API call.
*
* @return the handler that will produce the snippet
*/
public static Snippet httpRequest() {
return new HttpRequestSnippet();
}
/**
* Returns a handler that will produce a snippet containing the HTTP request for the
* API call. The given {@code attributes} will be available during snippet generation.
*
* @param attributes the attributes
* @return the handler that will produce the snippet
*/
public static Snippet httpRequest(Map<String, Object> attributes) {
return new HttpRequestSnippet(attributes);
}
/**
* Returns a handler that will produce a snippet containing the HTTP response for the
* API call.
* @return the handler that will produce the snippet
*/
public static Snippet httpResponse() {
return new HttpResponseSnippet();
}
/**
* Returns a handler that will produce a snippet containing the HTTP response for the
* API call. The given {@code attributes} will be available during snippet generation.
*
* @param attributes the attributes
* @return the handler that will produce the snippet
*/
public static Snippet httpResponse(Map<String, Object> attributes) {
return new HttpResponseSnippet(attributes);
}
}

View File

@@ -0,0 +1,182 @@
/*
* 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.http;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.util.StringUtils;
/**
* A {@link Snippet} that documents an HTTP request.
*
* @author Andy Wilkinson
*/
class HttpRequestSnippet extends TemplatedSnippet {
private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
HttpRequestSnippet() {
this(null);
}
HttpRequestSnippet(Map<String, Object> attributes) {
super("http-request", attributes);
}
@Override
public Map<String, Object> createModel(Operation operation) throws IOException {
Map<String, Object> model = new HashMap<String, Object>();
model.put("method", operation.getRequest().getMethod());
model.put(
"path",
operation.getRequest().getUri().getRawPath()
+ (StringUtils.hasText(operation.getRequest().getUri()
.getRawQuery()) ? "?"
+ operation.getRequest().getUri().getRawQuery()
: ""));
model.put("headers", getHeaders(operation.getRequest()));
model.put("requestBody", getRequestBody(operation.getRequest()));
return model;
}
private List<Map<String, String>> getHeaders(OperationRequest request) {
List<Map<String, String>> headers = new ArrayList<>();
if (requiresHostHeader(request)) {
headers.add(header(HttpHeaders.HOST, request.getUri().getHost()));
}
for (Entry<String, List<String>> header : request.getHeaders().entrySet()) {
for (String value : header.getValue()) {
if (header.getKey() == HttpHeaders.CONTENT_TYPE
&& !request.getParts().isEmpty()) {
headers.add(header(header.getKey(),
String.format("%s; boundary=%s", value, MULTIPART_BOUNDARY)));
}
else {
headers.add(header(header.getKey(), value));
}
}
}
if (requiresFormEncodingContentTypeHeader(request)) {
headers.add(header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_FORM_URLENCODED_VALUE));
}
return headers;
}
private String getRequestBody(OperationRequest request) throws IOException {
StringWriter httpRequest = new StringWriter();
PrintWriter writer = new PrintWriter(httpRequest);
if (request.getContent().length > 0) {
writer.println();
writer.print(new String(request.getContent()));
}
else if (isPutOrPost(request)) {
if (request.getParts().isEmpty()) {
String queryString = request.getParameters().toQueryString();
if (StringUtils.hasText(queryString)) {
writer.println();
writer.print(queryString);
}
}
else {
writeParts(request, writer);
}
}
return httpRequest.toString();
}
private boolean isPutOrPost(OperationRequest request) {
return HttpMethod.PUT.equals(request.getMethod())
|| HttpMethod.POST.equals(request.getMethod());
}
private void writeParts(OperationRequest request, PrintWriter writer)
throws IOException {
writer.println();
for (Entry<String, List<String>> parameter : request.getParameters().entrySet()) {
for (String value : parameter.getValue()) {
writePartBoundary(writer);
writePart(parameter.getKey(), value, null, writer);
writer.println();
}
}
for (OperationRequestPart part : request.getParts()) {
writePartBoundary(writer);
writePart(part, writer);
writer.println();
}
writeMultipartEnd(writer);
}
private void writePartBoundary(PrintWriter writer) {
writer.printf("--%s%n", MULTIPART_BOUNDARY);
}
private void writePart(OperationRequestPart part, PrintWriter writer)
throws IOException {
writePart(part.getName(), new String(part.getContent()), part.getHeaders()
.getContentType(), writer);
}
private void writePart(String name, String value, MediaType contentType,
PrintWriter writer) {
writer.printf("Content-Disposition: form-data; name=%s%n", name);
if (contentType != null) {
writer.printf("Content-Type: %s%n", contentType);
}
writer.println();
writer.print(value);
}
private void writeMultipartEnd(PrintWriter writer) {
writer.printf("--%s--", MULTIPART_BOUNDARY);
}
private boolean requiresHostHeader(OperationRequest request) {
return request.getHeaders().get(HttpHeaders.HOST) == null;
}
private boolean requiresFormEncodingContentTypeHeader(OperationRequest request) {
return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null
&& isPutOrPost(request) && !request.getParameters().isEmpty();
}
private Map<String, String> header(String name, String value) {
Map<String, String> header = new HashMap<>();
header.put("name", name);
header.put("value", value);
return header;
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.http;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.TemplatedSnippet;
/**
* A {@link Snippet} that documents an HTTP response.
*
* @author Andy Wilkinson
*/
class HttpResponseSnippet extends TemplatedSnippet {
HttpResponseSnippet() {
this(null);
}
HttpResponseSnippet(Map<String, Object> attributes) {
super("http-response", attributes);
}
@Override
public Map<String, Object> createModel(Operation operation) throws IOException {
OperationResponse response = operation.getResponse();
HttpStatus status = response.getStatus();
Map<String, Object> model = new HashMap<String, Object>();
model.put(
"responseBody",
response.getContent().length > 0 ? String.format("%n%s", new String(
response.getContent())) : "");
model.put("statusCode", status.value());
model.put("statusReason", status.getReasonPhrase());
model.put("headers", headers(response));
return model;
}
private List<Map<String, String>> headers(OperationResponse response) {
List<Map<String, String>> headers = new ArrayList<>();
for (Entry<String, List<String>> header : response.getHeaders().entrySet()) {
List<String> values = header.getValue();
for (String value : values) {
headers.add(header(header.getKey(), value));
}
}
return headers;
}
private Map<String, String> header(String name, String value) {
Map<String, String> header = new HashMap<>();
header.put("name", name);
header.put("value", value);
return header;
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.hypermedia;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.springframework.restdocs.operation.OperationResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Abstract base class for a {@link LinkExtractor} that extracts links from JSON
*
* @author Andy Wilkinson
*/
abstract class AbstractJsonLinkExtractor implements LinkExtractor {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
@SuppressWarnings("unchecked")
public Map<String, List<Link>> extractLinks(OperationResponse response)
throws IOException {
Map<String, Object> jsonContent = this.objectMapper.readValue(
new String(response.getContent()), Map.class);
return extractLinks(jsonContent);
}
protected abstract Map<String, List<Link>> extractLinks(Map<String, Object> json);
}

View File

@@ -0,0 +1,65 @@
/*
* 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.hypermedia;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* {@link LinkExtractor} that extracts links in Atom format.
*
* @author Andy Wilkinson
*/
@SuppressWarnings("unchecked")
class AtomLinkExtractor extends AbstractJsonLinkExtractor {
@Override
public Map<String, List<Link>> extractLinks(Map<String, Object> json) {
MultiValueMap<String, Link> extractedLinks = new LinkedMultiValueMap<>();
Object possibleLinks = json.get("links");
if (possibleLinks instanceof Collection) {
Collection<Object> linksCollection = (Collection<Object>) possibleLinks;
for (Object linkObject : linksCollection) {
if (linkObject instanceof Map) {
Link link = maybeCreateLink((Map<String, Object>) linkObject);
maybeStoreLink(link, extractedLinks);
}
}
}
return extractedLinks;
}
private static Link maybeCreateLink(Map<String, Object> linkMap) {
Object hrefObject = linkMap.get("href");
Object relObject = linkMap.get("rel");
if (relObject instanceof String && hrefObject instanceof String) {
return new Link((String) relObject, (String) hrefObject);
}
return null;
}
private static void maybeStoreLink(Link link,
MultiValueMap<String, Link> extractedLinks) {
if (link != null) {
extractedLinks.add(link.getRel(), link);
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.hypermedia;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.OperationResponse;
/**
* {@link LinkExtractor} that delegates to other link extractors based on the response's
* content type.
*
* @author Andy Wilkinson
*
*/
class ContentTypeLinkExtractor implements LinkExtractor {
private Map<MediaType, LinkExtractor> linkExtractors = new HashMap<>();
ContentTypeLinkExtractor() {
this.linkExtractors.put(MediaType.APPLICATION_JSON, new AtomLinkExtractor());
this.linkExtractors.put(HalLinkExtractor.HAL_MEDIA_TYPE, new HalLinkExtractor());
}
ContentTypeLinkExtractor(Map<MediaType, LinkExtractor> linkExtractors) {
this.linkExtractors.putAll(linkExtractors);
}
@Override
public Map<String, List<Link>> extractLinks(OperationResponse response)
throws IOException {
MediaType contentType = response.getHeaders().getContentType();
LinkExtractor extractorForContentType = getExtractorForContentType(contentType);
if (extractorForContentType != null) {
return extractorForContentType.extractLinks(response);
}
throw new IllegalStateException(
"No LinkExtractor has been provided and one is not available for the "
+ "content type " + contentType);
}
private LinkExtractor getExtractorForContentType(MediaType contentType) {
if (contentType != null) {
for (Entry<MediaType, LinkExtractor> entry : this.linkExtractors.entrySet()) {
if (contentType.isCompatibleWith(entry.getKey())) {
return entry.getValue();
}
}
}
return null;
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.hypermedia;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.MediaType;
/**
* {@link LinkExtractor} that extracts links in Hypermedia Application Language (HAL)
* format.
*
* @author Andy Wilkinson
*/
class HalLinkExtractor extends AbstractJsonLinkExtractor {
static final MediaType HAL_MEDIA_TYPE = new MediaType("application", "hal+json");
@Override
public Map<String, List<Link>> extractLinks(Map<String, Object> json) {
Map<String, List<Link>> extractedLinks = new LinkedHashMap<>();
Object possibleLinks = json.get("_links");
if (possibleLinks instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> links = (Map<String, Object>) possibleLinks;
for (Entry<String, Object> entry : links.entrySet()) {
String rel = entry.getKey();
extractedLinks.put(rel, convertToLinks(entry.getValue(), rel));
}
}
return extractedLinks;
}
private static List<Link> convertToLinks(Object object, String rel) {
List<Link> links = new ArrayList<>();
if (object instanceof Collection) {
@SuppressWarnings("unchecked")
Collection<Object> hrefObjects = (Collection<Object>) object;
for (Object hrefObject : hrefObjects) {
maybeAddLink(maybeCreateLink(rel, hrefObject), links);
}
}
else {
maybeAddLink(maybeCreateLink(rel, object), links);
}
return links;
}
private static Link maybeCreateLink(String rel, Object possibleHref) {
if (possibleHref instanceof String) {
return new Link(rel, (String) possibleHref);
}
return null;
}
private static void maybeAddLink(Link possibleLink, List<Link> links) {
if (possibleLink != null) {
links.add(possibleLink);
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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.hypermedia;
import java.util.Arrays;
import java.util.Map;
import org.springframework.restdocs.snippet.Snippet;
/**
* Static factory methods for documenting a RESTful API that utilizes Hypermedia.
*
* @author Andy Wilkinson
*/
public abstract class HypermediaDocumentation {
private HypermediaDocumentation() {
}
/**
* Creates a {@code LinkDescriptor} that describes a link with the given {@code rel}.
*
* @param rel The rel of the link
* @return a {@code LinkDescriptor} ready for further configuration
*/
public static LinkDescriptor linkWithRel(String rel) {
return new LinkDescriptor(rel);
}
/**
* Returns a handler that will produce a snippet documenting the links in the API
* call's response. Links will be extracted from the response automatically based on
* its content type.
*
* @param descriptors The descriptions of the response's links
* @return the handler
*/
public static Snippet links(LinkDescriptor... descriptors) {
return new LinksSnippet(new ContentTypeLinkExtractor(),
Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the links in the API
* call's response. The given {@code attributes} will be available during snippet
* generation. Links will be extracted from the response automatically based on its
* content type.
*
* @param attributes Attributes made available during rendering of the links snippet
* @param descriptors The descriptions of the response's links
* @return the handler
*/
public static Snippet links(Map<String, Object> attributes,
LinkDescriptor... descriptors) {
return new LinksSnippet(new ContentTypeLinkExtractor(), attributes,
Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the links in the API
* call's response. Links will be extracted from the response using the given
* {@code linkExtractor}.
*
* @param linkExtractor Used to extract the links from the response
* @param descriptors The descriptions of the response's links
* @return the handler
*/
public static Snippet links(LinkExtractor linkExtractor,
LinkDescriptor... descriptors) {
return new LinksSnippet(linkExtractor, Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the links in the API
* call's response. The given {@code attributes} will be available during snippet
* generation. Links will be extracted from the response using the given
* {@code linkExtractor}.
*
* @param attributes Attributes made available during rendering of the links snippet
* @param linkExtractor Used to extract the links from the response
* @param descriptors The descriptions of the response's links
* @return the handler
*/
public static Snippet links(LinkExtractor linkExtractor,
Map<String, Object> attributes, LinkDescriptor... descriptors) {
return new LinksSnippet(linkExtractor, attributes,
Arrays.asList(descriptors));
}
/**
* Returns a {@code LinkExtractor} capable of extracting links in Hypermedia
* Application Language (HAL) format where the links are found in a map named
* {@code _links}.
*
* @return The extract for HAL-style links
*/
public static LinkExtractor halLinks() {
return new HalLinkExtractor();
}
/**
* Returns a {@code LinkExtractor} capable of extracting links in Atom format where
* the links are found in an array named {@code links}.
*
* @return The extractor for Atom-style links
*/
public static LinkExtractor atomLinks() {
return new AtomLinkExtractor();
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.hypermedia;
import org.springframework.core.style.ToStringCreator;
/**
* Representation of a link used in a Hypermedia-based API
*
* @author Andy Wilkinson
*/
public class Link {
private final String rel;
private final String href;
/**
* Creates a new {@code Link} with the given {@code rel} and {@code href}
*
* @param rel The link's rel
* @param href The link's href
*/
public Link(String rel, String href) {
this.rel = rel;
this.href = href;
}
/**
* Returns the link's {@code rel}
* @return the link's {@code rel}
*/
public String getRel() {
return this.rel;
}
/**
* Returns the link's {@code href}
* @return the link's {@code href}
*/
public String getHref() {
return this.href;
}
@Override
public int hashCode() {
int prime = 31;
int result = 1;
result = prime * result + this.href.hashCode();
result = prime * result + this.rel.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Link other = (Link) obj;
if (!this.href.equals(other.href)) {
return false;
}
if (!this.rel.equals(other.rel)) {
return false;
}
return true;
}
@Override
public String toString() {
return new ToStringCreator(this).append("rel", this.rel)
.append("href", this.href).toString();
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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.hypermedia;
import java.util.HashMap;
import java.util.Map;
import org.springframework.restdocs.snippet.AbstractDescriptor;
/**
* A description of a link found in a hypermedia API
*
* @see HypermediaDocumentation#linkWithRel(String)
*
* @author Andy Wilkinson
*/
public class LinkDescriptor extends AbstractDescriptor<LinkDescriptor> {
private final String rel;
private String description;
private boolean optional;
LinkDescriptor(String rel) {
this.rel = rel;
}
/**
* Specifies the description of the link
*
* @param description The link's description
* @return {@code this}
*/
public LinkDescriptor description(String description) {
this.description = description;
return this;
}
/**
* Marks the link as optional
*
* @return {@code this}
*/
public LinkDescriptor optional() {
this.optional = true;
return this;
}
String getRel() {
return this.rel;
}
String getDescription() {
return this.description;
}
boolean isOptional() {
return this.optional;
}
Map<String, Object> toModel() {
Map<String, Object> model = new HashMap<>();
model.put("rel", this.rel);
model.put("description", this.description);
model.put("optional", this.optional);
model.putAll(getAttributes());
return model;
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.hypermedia;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.springframework.restdocs.operation.OperationResponse;
/**
* A {@code LinkExtractor} is used to extract {@link Link links} from a JSON response. The
* expected format of the links in the response is determined by the implementation.
*
* @author Andy Wilkinson
*
*/
public interface LinkExtractor {
/**
* Extract the links from the given {@code response}, returning a {@code Map} of links
* where the keys are the link rels.
*
* @param response The response from which the links are to be extracted
* @return The extracted links, keyed by rel
* @throws IOException if link extraction fails
*/
Map<String, List<Link>> extractLinks(OperationResponse response) throws IOException;
}

View File

@@ -0,0 +1,110 @@
/*
* 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.hypermedia;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.util.Assert;
/**
* A {@link Snippet} that documents a RESTful resource's links.
*
* @author Andy Wilkinson
*/
class LinksSnippet extends TemplatedSnippet {
private final Map<String, LinkDescriptor> descriptorsByRel = new LinkedHashMap<>();
private final Set<String> requiredRels = new HashSet<>();
private final LinkExtractor linkExtractor;
LinksSnippet(LinkExtractor linkExtractor, List<LinkDescriptor> descriptors) {
this(linkExtractor, null, descriptors);
}
LinksSnippet(LinkExtractor linkExtractor, Map<String, Object> attributes,
List<LinkDescriptor> descriptors) {
super("links", attributes);
this.linkExtractor = linkExtractor;
for (LinkDescriptor descriptor : descriptors) {
Assert.hasText(descriptor.getRel());
Assert.hasText(descriptor.getDescription());
this.descriptorsByRel.put(descriptor.getRel(), descriptor);
if (!descriptor.isOptional()) {
this.requiredRels.add(descriptor.getRel());
}
}
}
@Override
protected Map<String, Object> createModel(Operation operation) throws IOException {
OperationResponse response = operation.getResponse();
validate(this.linkExtractor.extractLinks(response));
Map<String, Object> model = new HashMap<>();
model.put("links", createLinksModel());
return model;
}
private void validate(Map<String, List<Link>> links) {
Set<String> actualRels = links.keySet();
Set<String> undocumentedRels = new HashSet<String>(actualRels);
undocumentedRels.removeAll(this.descriptorsByRel.keySet());
Set<String> missingRels = new HashSet<String>(this.requiredRels);
missingRels.removeAll(actualRels);
if (!undocumentedRels.isEmpty() || !missingRels.isEmpty()) {
String message = "";
if (!undocumentedRels.isEmpty()) {
message += "Links with the following relations were not documented: "
+ undocumentedRels;
}
if (!missingRels.isEmpty()) {
if (message.length() > 0) {
message += ". ";
}
message += "Links with the following relations were not found in the "
+ "response: " + missingRels;
}
throw new SnippetException(message);
}
}
private List<Map<String, Object>> createLinksModel() {
List<Map<String, Object>> model = new ArrayList<>();
for (Entry<String, LinkDescriptor> entry : this.descriptorsByRel.entrySet()) {
model.add(entry.getValue().toModel());
}
return model;
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.operation;
import java.util.Map;
/**
* Describes an operation performed on a RESTful service.
*
* @author Andy Wilkinson
*/
public interface Operation {
/**
* Returns a {@code Map} of attributes associated with the operation.
*
* @return the attributes
*/
Map<String, Object> getAttributes();
/**
* Returns the name of the operation.
*
* @return the name
*/
String getName();
/**
* Returns the request that was sent.
*
* @return the request
*/
OperationRequest getRequest();
/**
* Returns the response that was received.
*
* @return the response
*/
OperationResponse getResponse();
}

View File

@@ -0,0 +1,79 @@
/*
* 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.operation;
import java.net.URI;
import java.util.Collection;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
/**
* The request that was sent as part of performing an operation on a RESTful service.
*
* @author Andy Wilkinson
* @see Operation#getRequest()
*/
public interface OperationRequest {
/**
* Returns the contents of the request. If the request has no content an empty array
* is returned
*
* @return the contents, never {@code null}
*/
byte[] getContent();
/**
* Returns the headers that were included in the request.
*
* @return the headers
*/
HttpHeaders getHeaders();
/**
* Returns the HTTP method of the request
*
* @return the HTTP method
*/
HttpMethod getMethod();
/**
* Returns the request's parameters. For a {@code GET} request, the parameters are
* derived from the query string. For a {@code POST} request, the parameters are
* derived form the request's body.
*
* @return the parameters
*/
Parameters getParameters();
/**
* Returns the request's parts, provided that it is a multipart request. If not, then
* an empty {@link Collection} is returned.
*
* @return the parts
*/
Collection<OperationRequestPart> getParts();
/**
* Returns the request's URI.
*
* @return the URI
*/
URI getUri();
}

View File

@@ -0,0 +1,57 @@
/*
* 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.operation;
import org.springframework.http.HttpHeaders;
/**
* A part of a multipart request
*
* @author awilkinson
* @see OperationRequest#getParts()
*/
public interface OperationRequestPart {
/**
* Returns the name of the part.
*
* @return the name
*/
String getName();
/**
* Returns the name of the file that is being uploaded in this part.
*
* @return the name of the file
*/
String getSubmittedFileName();
/**
* Returns the contents of the part.
*
* @return the contents
*/
byte[] getContent();
/**
* Returns the part's headers.
*
* @return the headers
*/
HttpHeaders getHeaders();
}

View File

@@ -0,0 +1,52 @@
/*
* 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.operation;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
/**
* The response that was received as part of performing an operation on a RESTful service.
*
* @author Andy Wilkinson
* @see Operation
* @see Operation#getRequest()
*/
public interface OperationResponse {
/**
* Returns the status of the response.
*
* @return the status
*/
HttpStatus getStatus();
/**
* Returns the headers in the response.
*
* @return the headers
*/
HttpHeaders getHeaders();
/**
* Returns the contents of the response. If the response has no content an empty array
* is returned.
*
* @return the contents, never {@code null}
*/
byte[] getContent();
}

View File

@@ -0,0 +1,64 @@
/*
* 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.operation;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import org.springframework.util.LinkedMultiValueMap;
/**
* The parameters received in a request
*
* @author Andy Wilkinson
*/
@SuppressWarnings("serial")
public class Parameters extends LinkedMultiValueMap<String, String> {
/**
* Converts the parameters to a query string suitable for use in a URI or the body of
* a form-encoded request
*
* @return the query string
*/
public String toQueryString() {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, List<String>> entry : entrySet()) {
for (String value : entry.getValue()) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(urlEncodeUTF8(entry.getKey())).append('=')
.append(urlEncodeUTF8(value));
}
}
return sb.toString();
}
private static String urlEncodeUTF8(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("Unable to URL encode " + s + " using UTF-8",
ex);
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.operation;
import java.util.Map;
/**
* Standard implementation of {@link Operation}
*
* @author Andy Wilkinson
*/
public class StandardOperation implements Operation {
private final String name;
private final OperationRequest request;
private final OperationResponse response;
private final Map<String, Object> attributes;
/**
* Creates a new {@code StandardOperation}
*
* @param name the name of the operation
* @param request the request that was sent
* @param response the response that was received
* @param attributes attributes to associate with the operation
*/
public StandardOperation(String name, OperationRequest request,
OperationResponse response, Map<String, Object> attributes) {
this.name = name;
this.request = request;
this.response = response;
this.attributes = attributes;
}
@Override
public Map<String, Object> getAttributes() {
return this.attributes;
}
@Override
public String getName() {
return this.name;
}
@Override
public OperationRequest getRequest() {
return this.request;
}
@Override
public OperationResponse getResponse() {
return this.response;
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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.operation;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
/**
* Standard implementation of {@link OperationRequest}.
*
* @author Andy Wilkinson
*/
public class StandardOperationRequest implements OperationRequest {
private byte[] content;
private HttpHeaders headers;
private HttpMethod method;
private Parameters parameters;
private Collection<OperationRequestPart> parts;
private URI uri;
/**
* Creates a new request with the given {@code uri} and {@code method}. The request
* will have the given {@code headers}, {@code parameters}, and {@code parts}.
*
* @param uri the uri
* @param method the method
* @param content the content
* @param headers the headers
* @param parameters the parameters
* @param parts the parts
*/
public StandardOperationRequest(URI uri, HttpMethod method, byte[] content,
HttpHeaders headers, Parameters parameters,
Collection<OperationRequestPart> parts) {
this.uri = uri;
this.method = method;
this.content = content;
this.headers = headers;
this.parameters = parameters;
this.parts = parts;
}
@Override
public byte[] getContent() {
return Arrays.copyOf(this.content, this.content.length);
}
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
@Override
public HttpMethod getMethod() {
return this.method;
}
@Override
public Parameters getParameters() {
return this.parameters;
}
@Override
public Collection<OperationRequestPart> getParts() {
return Collections.unmodifiableCollection(this.parts);
}
@Override
public URI getUri() {
return this.uri;
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.operation;
import org.springframework.http.HttpHeaders;
/**
* Standard implementation of {@code OperationRequestPart}.
*
* @author Andy Wilkinson
*/
public class StandardOperationRequestPart implements OperationRequestPart {
private final String name;
private final String submittedFileName;
private final byte[] content;
private final HttpHeaders headers;
/**
* Creates a new {@code StandardOperationRequestPart} with the given {@code name}.
*
* @param name the name of the part
* @param submittedFileName the name of the file being uploaded by this part
* @param content the contents of the part
* @param headers the headers of the part
*/
public StandardOperationRequestPart(String name, String submittedFileName,
byte[] content, HttpHeaders headers) {
this.name = name;
this.submittedFileName = submittedFileName;
this.content = content;
this.headers = headers;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getSubmittedFileName() {
return this.submittedFileName;
}
@Override
public byte[] getContent() {
return this.content;
}
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.operation;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
/**
* Standard implementation of {@link OperationResponse}.
*
* @author Andy Wilkinson
*/
public class StandardOperationResponse implements OperationResponse {
private final HttpStatus status;
private final HttpHeaders headers;
private final byte[] content;
/**
* Creates a new response with the given {@code status}, {@code headers}, and
* {@code content}.
*
* @param status the status of the response
* @param headers the headers of the response
* @param content the content of the response
*/
public StandardOperationResponse(HttpStatus status, HttpHeaders headers,
byte[] content) {
this.status = status;
this.headers = headers;
this.content = content;
}
@Override
public HttpStatus getStatus() {
return this.status;
}
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
@Override
public byte[] getContent() {
return this.content;
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.operation.preprocess;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationResponse;
/**
* A {@code ContentModifier} modifies the content of an {@link OperationRequest} or
* {@link OperationResponse} during the preprocessing that is performed prior to
* documentation generation.
*
* @author Andy Wilkinson
* @see ContentModifyingOperationPreprocessor
*/
interface ContentModifier {
/**
* Returns modified content based on the given {@code originalContent}
*
* @param originalContent the original content
* @return the modified content
*/
byte[] modifyContent(byte[] originalContent);
}

View File

@@ -0,0 +1,65 @@
/*
* 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.operation.preprocess;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.StandardOperationRequest;
import org.springframework.restdocs.operation.StandardOperationResponse;
/**
* An {@link OperationPreprocessor} that applies a {@link ContentModifier} to the content
* of the request or response.
*
* @author Andy Wilkinson
*/
class ContentModifyingOperationPreprocessor implements OperationPreprocessor {
private final ContentModifier contentModifier;
ContentModifyingOperationPreprocessor(ContentModifier contentModifier) {
this.contentModifier = contentModifier;
}
@Override
public OperationRequest preprocess(OperationRequest request) {
byte[] modifiedContent = this.contentModifier.modifyContent(request.getContent());
return new StandardOperationRequest(request.getUri(), request.getMethod(),
modifiedContent,
getUpdatedHeaders(request.getHeaders(), modifiedContent),
request.getParameters(), request.getParts());
}
@Override
public OperationResponse preprocess(OperationResponse response) {
byte[] modifiedContent = this.contentModifier
.modifyContent(response.getContent());
return new StandardOperationResponse(response.getStatus(), getUpdatedHeaders(
response.getHeaders(), modifiedContent), modifiedContent);
}
private HttpHeaders getUpdatedHeaders(HttpHeaders headers, byte[] updatedContent) {
HttpHeaders updatedHeaders = new HttpHeaders();
updatedHeaders.putAll(headers);
if (updatedHeaders.getContentLength() > -1) {
updatedHeaders.setContentLength(updatedContent.length);
}
return updatedHeaders;
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.operation.preprocess;
import java.util.List;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.util.Assert;
/**
* An {@link OperationRequestPreprocessor} that delgates to one or more
* {@link OperationPreprocessor OperationPreprocessors} to preprocess an
* {@link OperationRequest}.
*
* @author Andy Wilkinson
*
*/
class DelegatingOperationRequestPreprocessor implements OperationRequestPreprocessor {
private final List<OperationPreprocessor> delegates;
/**
* Creates a new {@code DelegatingOperationRequestPreprocessor} that will delegate to
* the given {@code delegates} by calling
* {@link OperationPreprocessor#preprocess(OperationRequest)}.
*
* @param delegates the delegates
*/
DelegatingOperationRequestPreprocessor(List<OperationPreprocessor> delegates) {
Assert.notNull(delegates, "delegates must be non-null");
this.delegates = delegates;
}
@Override
public OperationRequest preprocess(OperationRequest operationRequest) {
OperationRequest preprocessedRequest = operationRequest;
for (OperationPreprocessor delegate : this.delegates) {
preprocessedRequest = delegate.preprocess(preprocessedRequest);
}
return preprocessedRequest;
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.operation.preprocess;
import java.util.List;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.util.Assert;
/**
* An {@link OperationResponsePreprocessor} that delgates to one or more
* {@link OperationPreprocessor OperationPreprocessors} to preprocess an
* {@link OperationResponse}.
*
* @author Andy Wilkinson
*/
class DelegatingOperationResponsePreprocessor implements OperationResponsePreprocessor {
private final List<OperationPreprocessor> delegates;
/**
* Creates a new {@code DelegatingOperationResponsePreprocessor} that will delegate to
* the given {@code delegates} by calling
* {@link OperationPreprocessor#preprocess(OperationResponse)}.
*
* @param delegates the delegates
*/
DelegatingOperationResponsePreprocessor(List<OperationPreprocessor> delegates) {
Assert.notNull(delegates, "delegates must be non-null");
this.delegates = delegates;
}
@Override
public OperationResponse preprocess(OperationResponse response) {
OperationResponse preprocessedResponse = response;
for (OperationPreprocessor delegate : this.delegates) {
preprocessedResponse = delegate.preprocess(preprocessedResponse);
}
return preprocessedResponse;
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.operation.preprocess;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.springframework.http.HttpHeaders;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.StandardOperationRequest;
import org.springframework.restdocs.operation.StandardOperationResponse;
/**
* An {@link OperationPreprocessor} that removes headers
*
* @author Andy Wilkinson
*/
class HeaderRemovingOperationPreprocessor implements OperationPreprocessor {
private final Set<String> headersToRemove;
HeaderRemovingOperationPreprocessor(String... headersToRemove) {
this.headersToRemove = new HashSet<>(Arrays.asList(headersToRemove));
}
@Override
public OperationResponse preprocess(OperationResponse response) {
return new StandardOperationResponse(response.getStatus(),
removeHeaders(response.getHeaders()), response.getContent());
}
@Override
public OperationRequest preprocess(OperationRequest request) {
return new StandardOperationRequest(request.getUri(), request.getMethod(),
request.getContent(), removeHeaders(request.getHeaders()),
request.getParameters(), request.getParts());
}
private HttpHeaders removeHeaders(HttpHeaders originalHeaders) {
HttpHeaders processedHeaders = new HttpHeaders();
processedHeaders.putAll(originalHeaders);
for (String headerToRemove : this.headersToRemove) {
processedHeaders.remove(headerToRemove);
}
return processedHeaders;
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.operation.preprocess;
import java.util.regex.Pattern;
/**
* A content modifier the masks the {@code href} of any hypermedia links
*
* @author Andy Wilkinson
*/
class LinkMaskingContentModifier implements ContentModifier {
private static final String DEFAULT_MASK = "...";
private static final Pattern LINK_HREF = Pattern.compile(
"\"href\"\\s*:\\s*\"(.*?)\"", Pattern.DOTALL);
private final ContentModifier contentModifier;
LinkMaskingContentModifier() {
this(DEFAULT_MASK);
}
LinkMaskingContentModifier(String mask) {
this.contentModifier = new PatternReplacingContentModifier(LINK_HREF, mask);
}
@Override
public byte[] modifyContent(byte[] originalContent) {
return this.contentModifier.modifyContent(originalContent);
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.operation.preprocess;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationResponse;
/**
* An {@code OperationPreprocessor} processes the {@link OperationRequest} and
* {@link OperationResponse} of an {@link Operation} prior to it being documented.
*
* @author Andy Wilkinson
*/
public interface OperationPreprocessor {
/**
* Processes the given {@code request}
*
* @param request the request to process
* @return the processed request
*/
OperationRequest preprocess(OperationRequest request);
/**
* Processes the given {@code response}
*
* @param response the response to process
* @return the processed response
*/
OperationResponse preprocess(OperationResponse response);
}

View File

@@ -0,0 +1,38 @@
/*
* 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.operation.preprocess;
import org.springframework.restdocs.operation.OperationRequest;
/**
* An {@code OperationRequestPreprocessor} is used to modify an {@code OperationRequest}
* prior to it being documented.
*
* @author Andy Wilkinson
*/
public interface OperationRequestPreprocessor {
/**
* Processes and potentially modifies the given {@code request} before it is
* documented.
*
* @param request the request
* @return the modified request
*/
OperationRequest preprocess(OperationRequest request);
}

View File

@@ -0,0 +1,38 @@
/*
* 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.operation.preprocess;
import org.springframework.restdocs.operation.OperationResponse;
/**
* An {@code OperationRequestPreprocessor} is used to modify an {@code OperationRequest}
* prior to it being documented.
*
* @author Andy Wilkinson
*/
public interface OperationResponsePreprocessor {
/**
* Processes and potentially modifies the given {@code response} before it is
* documented.
*
* @param response the response
* @return the modified response
*/
OperationResponse preprocess(OperationResponse response);
}

View File

@@ -0,0 +1,64 @@
/*
* 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.operation.preprocess;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A {@link ContentModifier} that modifies the content by replacing occurrences of a
* regular expression {@link Pattern}.
*
* @author Andy Wilkinson
* @author Dewet Diener
*/
class PatternReplacingContentModifier implements ContentModifier {
private final Pattern pattern;
private final String replacement;
/**
* Creates a new {@link PatternReplacingContentModifier} that will replace occurences
* the given {@code pattern} with the given {@code replacement}.
*
* @param pattern the pattern
* @param replacement the replacement
*/
PatternReplacingContentModifier(Pattern pattern, String replacement) {
this.pattern = pattern;
this.replacement = replacement;
}
@Override
public byte[] modifyContent(byte[] content) {
String original = new String(content);
Matcher matcher = this.pattern.matcher(original);
StringBuilder buffer = new StringBuilder();
int previous = 0;
while (matcher.find()) {
buffer.append(original.substring(previous, matcher.start(1)));
buffer.append(this.replacement);
previous = matcher.end(1);
}
if (previous < original.length()) {
buffer.append(original.substring(previous));
}
return buffer.toString().getBytes();
}
}

View File

@@ -0,0 +1,122 @@
/*
* 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.operation.preprocess;
import java.util.Arrays;
import java.util.regex.Pattern;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationResponse;
/**
* Static factory methods for creating {@link OperationPreprocessor
* OperationPreprocessors} that can be applied to an {@link Operation Operation's}
* {@link OperationRequest request} or {@link OperationResponse response} before it is
* documented.
*
* @author Andy Wilkinson
*/
public class Preprocessors {
private Preprocessors() {
}
/**
* Returns an {@link OperationRequestPreprocessor} that will preprocess the request by
* applying the given {@code preprocessors} to it.
*
* @param preprocessors the preprocessors
* @return the request preprocessor
*/
public static OperationRequestPreprocessor preprocessRequest(
OperationPreprocessor... preprocessors) {
return new DelegatingOperationRequestPreprocessor(Arrays.asList(preprocessors));
}
/**
* Returns an {@link OperationResponsePreprocessor} that will preprocess the response
* by applying the given {@code preprocessors} to it.
*
* @param preprocessors the preprocessors
* @return the response preprocessor
*/
public static OperationResponsePreprocessor preprocessResponse(
OperationPreprocessor... preprocessors) {
return new DelegatingOperationResponsePreprocessor(Arrays.asList(preprocessors));
}
/**
* Returns an {@code OperationPreprocessor} that will pretty print the content of the
* request or response.
*
* @return the preprocessor
*/
public static OperationPreprocessor prettyPrint() {
return new ContentModifyingOperationPreprocessor(
new PrettyPrintingContentModifier());
}
/**
* Returns an {@code OperationPreprocessor} that will remove headers from the request
* or response.
*
* @param headersToRemove the names of the headers to remove
* @return the preprocessor
*/
public static OperationPreprocessor removeHeaders(String... headersToRemove) {
return new HeaderRemovingOperationPreprocessor(headersToRemove);
}
/**
* Returns an {@code OperationPreprocessor} that will mask the href of hypermedia
* links in the request or response.
*
* @return the preprocessor
*/
public static OperationPreprocessor maskLinks() {
return new ContentModifyingOperationPreprocessor(new LinkMaskingContentModifier());
}
/**
* Returns an {@code OperationPreprocessor} that will mask the href of hypermedia
* links in the request or response.
*
* @param mask the link mask
* @return the preprocessor
*/
public static OperationPreprocessor maskLinks(String mask) {
return new ContentModifyingOperationPreprocessor(new LinkMaskingContentModifier(
mask));
}
/**
* Returns an {@code OperationPreprocessor} that will modify the content of the
* request or response by replacing occurences of the given {@code pattern} with the
* given {@code replacement}
*
* @param pattern the pattern
* @param replacement the replacement
* @return the preprocessor
*/
public static OperationPreprocessor replacePattern(Pattern pattern, String replacement) {
return new ContentModifyingOperationPreprocessor(
new PatternReplacingContentModifier(pattern, replacement));
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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.operation.preprocess;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* A {@link ContentModifier} that modifies the content by pretty printing it.
*
* @author Andy Wilkinson
*/
public class PrettyPrintingContentModifier implements ContentModifier {
private static final List<PrettyPrinter> PRETTY_PRINTERS = Collections
.unmodifiableList(Arrays.asList(new JsonPrettyPrinter(),
new XmlPrettyPrinter()));
@Override
public byte[] modifyContent(byte[] originalContent) {
for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) {
try {
return prettyPrinter.prettyPrint(originalContent).getBytes();
}
catch (Exception ex) {
// Continue
}
}
return originalContent;
}
private interface PrettyPrinter {
String prettyPrint(byte[] content) throws Exception;
}
private static final class XmlPrettyPrinter implements PrettyPrinter {
@Override
public String prettyPrint(byte[] original) throws Exception {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount",
"4");
transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes");
StringWriter transformed = new StringWriter();
transformer.transform(new StreamSource(new ByteArrayInputStream(original)),
new StreamResult(transformed));
return transformed.toString();
}
}
private static final class JsonPrettyPrinter implements PrettyPrinter {
@Override
public String prettyPrint(byte[] original) throws IOException {
ObjectMapper objectMapper = new ObjectMapper().configure(
SerializationFeature.INDENT_OUTPUT, true);
return objectMapper.writeValueAsString(objectMapper.readTree(original));
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* 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.payload;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link TemplatedSnippet} that produces a snippet documenting a RESTful resource's
* request or response fields.
*
* @author Andreas Evers
* @author Andy Wilkinson
*/
public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
private List<FieldDescriptor> fieldDescriptors;
AbstractFieldsSnippet(String type, Map<String, Object> attributes,
List<FieldDescriptor> descriptors) {
super(type + "-fields", attributes);
for (FieldDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getPath());
Assert.hasText(descriptor.getDescription());
}
this.fieldDescriptors = descriptors;
}
@Override
protected Map<String, Object> createModel(Operation operation) throws IOException {
MediaType contentType = getContentType(operation);
ContentHandler contentHandler;
if (contentType != null
&& MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
contentHandler = new XmlContentHandler(getContent(operation));
}
else {
contentHandler = new JsonContentHandler(getContent(operation));
}
validateFieldDocumentation(contentHandler);
for (FieldDescriptor descriptor : this.fieldDescriptors) {
if (descriptor.getType() == null) {
descriptor.type(contentHandler.determineFieldType(descriptor.getPath()));
}
}
Map<String, Object> model = new HashMap<>();
List<Map<String, Object>> fields = new ArrayList<>();
model.put("fields", fields);
for (FieldDescriptor descriptor : this.fieldDescriptors) {
fields.add(descriptor.toModel());
}
return model;
}
private void validateFieldDocumentation(ContentHandler payloadHandler) {
List<FieldDescriptor> missingFields = payloadHandler
.findMissingFields(this.fieldDescriptors);
String undocumentedPayload = payloadHandler
.getUndocumentedContent(this.fieldDescriptors);
if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) {
String message = "";
if (StringUtils.hasText(undocumentedPayload)) {
message += String.format("The following parts of the payload were"
+ " not documented:%n%s", undocumentedPayload);
}
if (!missingFields.isEmpty()) {
if (message.length() > 0) {
message += String.format("%n");
}
List<String> paths = new ArrayList<String>();
for (FieldDescriptor fieldDescriptor : missingFields) {
paths.add(fieldDescriptor.getPath());
}
message += "Fields with the following paths were not found in the"
+ " payload: " + paths;
}
throw new SnippetException(message);
}
}
protected abstract MediaType getContentType(Operation operation);
protected abstract byte[] getContent(Operation operation) throws IOException;
}

View File

@@ -0,0 +1,61 @@
/*
* 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.payload;
import java.util.List;
/**
* A handler for the content of a request or response
*
* @author Andy Wilkinson
*/
interface ContentHandler {
/**
* Finds the fields that are missing from the handler's payload. A field is missing if
* it is described by one of the {@code fieldDescriptors} but is not present in the
* payload.
*
* @param fieldDescriptors the descriptors
* @return descriptors for the fields that are missing from the payload
* @throws PayloadHandlingException if a failure occurs
*/
List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors);
/**
* Returns modified content, formatted as a String, that only contains the fields that
* are undocumented. A field is undocumented if it is present in the handler's content
* but is not described by the given {@code fieldDescriptors}. If the content is
* completely documented, {@code null} is returned
*
* @param fieldDescriptors the descriptors
* @return the undocumented content, or {@code null} if all of the content is
* documented
* @throws PayloadHandlingException if a failure occurs
*/
String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors);
/**
* Returns the type of the field with the given {@code path} based on the content of
* the payload.
*
* @param path the field path
* @return the type of the field
*/
Object determineFieldType(String path);
}

View File

@@ -0,0 +1,105 @@
/*
* 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.payload;
import java.util.HashMap;
import java.util.Map;
import org.springframework.restdocs.snippet.AbstractDescriptor;
/**
* A description of a field found in a request or response payload
*
* @see PayloadDocumentation#fieldWithPath(String)
*
* @author Andreas Evers
* @author Andy Wilkinson
*/
public class FieldDescriptor extends AbstractDescriptor<FieldDescriptor> {
private final String path;
private Object type;
private boolean optional;
private String description;
FieldDescriptor(String path) {
this.path = path;
}
/**
* Specifies the type of the field. When documenting a JSON payload, the
* {@link JsonFieldType} enumeration will typically be used.
*
* @param type The type of the field
* @return {@code this}
* @see JsonFieldType
*/
public FieldDescriptor type(Object type) {
this.type = type;
return this;
}
/**
* Marks the field as optional
*
* @return {@code this}
*/
public FieldDescriptor optional() {
this.optional = true;
return this;
}
/**
* Specifies the description of the field
*
* @param description The field's description
* @return {@code this}
*/
public FieldDescriptor description(String description) {
this.description = description;
return this;
}
String getPath() {
return this.path;
}
Object getType() {
return this.type;
}
boolean isOptional() {
return this.optional;
}
String getDescription() {
return this.description;
}
Map<String, Object> toModel() {
Map<String, Object> model = new HashMap<String, Object>();
model.put("path", this.path);
model.put("type", this.type.toString());
model.put("description", this.description);
model.put("optional", this.optional);
model.putAll(this.getAttributes());
return model;
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.payload;
/**
* A {@code FieldDoesNotExistException} is thrown when a requested field does not exist in
* a payload.
*
* @author Andy Wilkinson
*/
@SuppressWarnings("serial")
public class FieldDoesNotExistException extends RuntimeException {
/**
* Creates a new {@code FieldDoesNotExistException} that indicates that the field with
* the given {@code fieldPath} does not exist.
*
* @param fieldPath the path of the field that does not exist
*/
public FieldDoesNotExistException(JsonFieldPath fieldPath) {
super("The payload does not contain a field with the path '" + fieldPath + "'");
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.payload;
/**
* A {@code FieldTypeRequiredException} is thrown when a field's type cannot be determined
* automatically and, therefore, must be explicitly provided.
*
* @author Andy Wilkinson
*/
@SuppressWarnings("serial")
public class FieldTypeRequiredException extends RuntimeException {
/**
* Creates a new {@code FieldTypeRequiredException} indicating that a type is required
* for the reason described in the given {@code message}.
*
* @param message the message
*/
public FieldTypeRequiredException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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.payload;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* A {@link ContentHandler} for JSON content
*
* @author Andy Wilkinson
*/
class JsonContentHandler implements ContentHandler {
private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
private final ObjectMapper objectMapper = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
private final byte[] rawContent;
JsonContentHandler(byte[] content) throws IOException {
this.rawContent = content;
}
@Override
public List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors) {
List<FieldDescriptor> missingFields = new ArrayList<FieldDescriptor>();
Object payload = readContent();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
if (!fieldDescriptor.isOptional()
&& !this.fieldProcessor.hasField(
JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) {
missingFields.add(fieldDescriptor);
}
}
return missingFields;
}
@Override
public String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors) {
Object content = readContent();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath());
this.fieldProcessor.remove(path, content);
}
if (!isEmpty(content)) {
try {
return this.objectMapper.writeValueAsString(content);
}
catch (JsonProcessingException ex) {
throw new PayloadHandlingException(ex);
}
}
return null;
}
private Object readContent() {
try {
return new ObjectMapper().readValue(this.rawContent, Object.class);
}
catch (IOException ex) {
throw new PayloadHandlingException(ex);
}
}
private boolean isEmpty(Object object) {
if (object instanceof Map) {
return ((Map<?, ?>) object).isEmpty();
}
return ((List<?>) object).isEmpty();
}
@Override
public Object determineFieldType(String path) {
try {
return new JsonFieldTypeResolver().resolveFieldType(path, readContent());
}
catch (FieldDoesNotExistException ex) {
String message = "Cannot determine the type of the field '" + path + "' as"
+ " it is not present in the payload. Please provide a type using"
+ " FieldDescriptor.type(Object type).";
throw new FieldTypeRequiredException(message);
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* 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.payload;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A path that identifies a field in a JSON payload
*
* @author Andy Wilkinson
*
*/
final class JsonFieldPath {
private static final Pattern ARRAY_INDEX_PATTERN = Pattern
.compile("\\[([0-9]+|\\*){0,1}\\]");
private final String rawPath;
private final List<String> segments;
private final boolean precise;
private JsonFieldPath(String rawPath, List<String> segments, boolean precise) {
this.rawPath = rawPath;
this.segments = segments;
this.precise = precise;
}
boolean isPrecise() {
return this.precise;
}
List<String> getSegments() {
return this.segments;
}
@Override
public String toString() {
return this.rawPath;
}
static JsonFieldPath compile(String path) {
List<String> segments = extractSegments(path);
return new JsonFieldPath(path, segments, matchesSingleValue(segments));
}
static boolean isArraySegment(String segment) {
return ARRAY_INDEX_PATTERN.matcher(segment).matches();
}
static boolean matchesSingleValue(List<String> segments) {
for (String segment : segments) {
if (isArraySegment(segment)) {
return false;
}
}
return true;
}
private static List<String> extractSegments(String path) {
Matcher matcher = ARRAY_INDEX_PATTERN.matcher(path);
StringBuilder buffer = new StringBuilder();
int previous = 0;
while (matcher.find()) {
appendWithSeparatorIfNecessary(buffer,
path.substring(previous, matcher.start(0)));
appendWithSeparatorIfNecessary(buffer, matcher.group());
previous = matcher.end(0);
}
if (previous < path.length()) {
appendWithSeparatorIfNecessary(buffer, path.substring(previous));
}
String processedPath = buffer.toString();
return Arrays.asList(processedPath.indexOf('.') > -1 ? processedPath.split("\\.")
: new String[] { processedPath });
}
private static void appendWithSeparatorIfNecessary(StringBuilder buffer,
String toAppend) {
if (buffer.length() > 0 && (buffer.lastIndexOf(".") != buffer.length() - 1)
&& !toAppend.startsWith(".")) {
buffer.append(".");
}
buffer.append(toAppend);
}
}

View File

@@ -0,0 +1,246 @@
/*
* 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.payload;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
/**
* A {@code JsonFieldProcessor} processes a payload's fields, allowing them to be
* extracted and removed
*
* @author Andy Wilkinson
*
*/
final class JsonFieldProcessor {
boolean hasField(JsonFieldPath fieldPath, Object payload) {
final AtomicReference<Boolean> hasField = new AtomicReference<Boolean>(false);
traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() {
@Override
public void foundMatch(Match match) {
hasField.set(true);
}
});
return hasField.get();
}
Object extract(JsonFieldPath path, Object payload) {
final List<Object> matches = new ArrayList<Object>();
traverse(new ProcessingContext(payload, path), new MatchCallback() {
@Override
public void foundMatch(Match match) {
matches.add(match.getValue());
}
});
if (matches.isEmpty()) {
throw new FieldDoesNotExistException(path);
}
if (path.isPrecise()) {
return matches.get(0);
}
else {
return matches;
}
}
void remove(final JsonFieldPath path, Object payload) {
traverse(new ProcessingContext(payload, path), new MatchCallback() {
@Override
public void foundMatch(Match match) {
match.remove();
}
});
}
private void traverse(ProcessingContext context, MatchCallback matchCallback) {
final String segment = context.getSegment();
if (JsonFieldPath.isArraySegment(segment)) {
if (context.getPayload() instanceof List) {
handleListPayload(context, matchCallback);
}
}
else if (context.getPayload() instanceof Map
&& ((Map<?, ?>) context.getPayload()).containsKey(segment)) {
handleMapPayload(context, matchCallback);
}
}
private void handleListPayload(ProcessingContext context, MatchCallback matchCallback) {
List<?> list = context.getPayload();
final Iterator<?> items = list.iterator();
if (context.isLeaf()) {
while (items.hasNext()) {
Object item = items.next();
matchCallback.foundMatch(new ListMatch(items, list, item, context
.getParentMatch()));
}
}
else {
while (items.hasNext()) {
Object item = items.next();
traverse(context.descend(item, new ListMatch(items, list, item,
context.parent)), matchCallback);
}
}
}
private void handleMapPayload(ProcessingContext context, MatchCallback matchCallback) {
Map<?, ?> map = context.getPayload();
Object item = map.get(context.getSegment());
MapMatch mapMatch = new MapMatch(item, map, context.getSegment(),
context.getParentMatch());
if (context.isLeaf()) {
matchCallback.foundMatch(mapMatch);
}
else {
traverse(context.descend(item, mapMatch), matchCallback);
}
}
private static final class MapMatch implements Match {
private final Object item;
private final Map<?, ?> map;
private final String segment;
private final Match parent;
private MapMatch(Object item, Map<?, ?> map, String segment, Match parent) {
this.item = item;
this.map = map;
this.segment = segment;
this.parent = parent;
}
@Override
public Object getValue() {
return this.item;
}
@Override
public void remove() {
this.map.remove(this.segment);
if (this.map.isEmpty() && this.parent != null) {
this.parent.remove();
}
}
}
private static final class ListMatch implements Match {
private final Iterator<?> items;
private final List<?> list;
private final Object item;
private final Match parent;
private ListMatch(Iterator<?> items, List<?> list, Object item, Match parent) {
this.items = items;
this.list = list;
this.item = item;
this.parent = parent;
}
@Override
public Object getValue() {
return this.item;
}
@Override
public void remove() {
this.items.remove();
if (this.list.isEmpty() && this.parent != null) {
this.parent.remove();
}
}
}
private interface MatchCallback {
void foundMatch(Match match);
}
private interface Match {
Object getValue();
void remove();
}
private static final class ProcessingContext {
private final Object payload;
private final List<String> segments;
private final Match parent;
private final JsonFieldPath path;
private ProcessingContext(Object payload, JsonFieldPath path) {
this(payload, path, null, null);
}
private ProcessingContext(Object payload, JsonFieldPath path, List<String> segments,
Match parent) {
this.payload = payload;
this.path = path;
this.segments = segments == null ? path.getSegments() : segments;
this.parent = parent;
}
private String getSegment() {
return this.segments.get(0);
}
@SuppressWarnings("unchecked")
private <T> T getPayload() {
return (T) this.payload;
}
private boolean isLeaf() {
return this.segments.size() == 1;
}
private Match getParentMatch() {
return this.parent;
}
private ProcessingContext descend(Object payload, Match match) {
return new ProcessingContext(payload, this.path, this.segments.subList(1,
this.segments.size()), match);
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.payload;
import java.util.Locale;
import org.springframework.util.StringUtils;
/**
* An enumeration of the possible types for a field in a JSON request or response payload
*
* @author Andy Wilkinson
*/
public enum JsonFieldType {
ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING, VARIES;
@Override
public String toString() {
return StringUtils.capitalize(this.name().toLowerCase(Locale.ENGLISH));
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.payload;
import java.util.Collection;
import java.util.Map;
/**
* Resolves the type of a field in a JSON request or response payload
*
* @author Andy Wilkinson
*/
class JsonFieldTypeResolver {
private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
JsonFieldType resolveFieldType(String path, Object payload) {
JsonFieldPath fieldPath = JsonFieldPath.compile(path);
Object field = this.fieldProcessor.extract(fieldPath, payload);
if (field instanceof Collection && !fieldPath.isPrecise()) {
JsonFieldType commonType = null;
for (Object item : (Collection<?>) field) {
JsonFieldType fieldType = determineFieldType(item);
if (commonType == null) {
commonType = fieldType;
}
else if (fieldType != commonType) {
return JsonFieldType.VARIES;
}
}
return commonType;
}
return determineFieldType(this.fieldProcessor.extract(fieldPath, payload));
}
private JsonFieldType determineFieldType(Object fieldValue) {
if (fieldValue == null) {
return JsonFieldType.NULL;
}
if (fieldValue instanceof String) {
return JsonFieldType.STRING;
}
if (fieldValue instanceof Map) {
return JsonFieldType.OBJECT;
}
if (fieldValue instanceof Collection) {
return JsonFieldType.ARRAY;
}
if (fieldValue instanceof Boolean) {
return JsonFieldType.BOOLEAN;
}
return JsonFieldType.NUMBER;
}
}

View File

@@ -0,0 +1,183 @@
/*
* 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.payload;
import java.util.Arrays;
import java.util.Map;
import org.springframework.restdocs.snippet.Snippet;
/**
* Static factory methods for documenting a RESTful API's request and response payloads.
*
* @author Andreas Evers
* @author Andy Wilkinson
*/
public abstract class PayloadDocumentation {
private PayloadDocumentation() {
}
/**
* Creates a {@code FieldDescriptor} that describes a field with the given
* {@code path}.
* <p>
* When documenting an XML payload, the {@code path} uses XPath, i.e. '/' is used to
* descend to a child node.
* <p>
* When documenting a JSON payload, the {@code path} uses '.' to descend into a child
* object and ' {@code []}' to descend into an array. For example, with this JSON
* payload:
*
* <pre>
* {
* "a":{
* "b":[
* {
* "c":"one"
* },
* {
* "c":"two"
* },
* {
* "d":"three"
* }
* ]
* }
* }
* </pre>
*
* The following paths are all present:
*
* <table summary="Paths and their values">
* <tr>
* <th>Path</th>
* <th>Value</th>
* </tr>
* <tr>
* <td>{@code a}</td>
* <td>An object containing "b"</td>
* </tr>
* <tr>
* <td>{@code a.b}</td>
* <td>An array containing three objects</td>
* </tr>
* <tr>
* <td>{@code a.b[]}</td>
* <td>An array containing three objects</td>
* </tr>
* <tr>
* <td>{@code a.b[].c}</td>
* <td>An array containing the strings "one" and "two"</td>
* </tr>
* <tr>
* <td>{@code a.b[].d}</td>
* <td>The string "three"</td>
* </tr>
* </table>
*
* @param path The path of the field
* @return a {@code FieldDescriptor} ready for further configuration
*/
public static FieldDescriptor fieldWithPath(String path) {
return new FieldDescriptor(path);
}
/**
* Returns a handler that will produce a snippet documenting the fields of the API
* call's request.
* <p>
* If a field is present in the request, but is not documented by one of the
* descriptors, a failure will occur when the handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the request,
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param descriptors The descriptions of the request's fields
* @return the handler
* @see #fieldWithPath(String)
*/
public static Snippet requestFields(FieldDescriptor... descriptors) {
return new RequestFieldsSnippet(Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the fields of the API
* call's request. The given {@code attributes} will be available during snippet
* generation.
* <p>
* If a field is present in the request, but is not documented by one of the
* descriptors, a failure will occur when the handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the request,
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param attributes Attributes made available during rendering of the snippet
* @param descriptors The descriptions of the request's fields
* @return the handler
* @see #fieldWithPath(String)
*/
public static Snippet requestFields(Map<String, Object> attributes,
FieldDescriptor... descriptors) {
return new RequestFieldsSnippet(attributes, Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the fields of the API
* call's response.
* <p>
* If a field is present in the response, but is not documented by one of the
* descriptors, a failure will occur when the handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the response,
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param descriptors The descriptions of the response's fields
* @return the handler
* @see #fieldWithPath(String)
*/
public static Snippet responseFields(FieldDescriptor... descriptors) {
return new ResponseFieldsSnippet(Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the fields of the API
* call's response. The given {@code attributes} will be available during snippet
* generation.
* <p>
* If a field is present in the response, but is not documented by one of the
* descriptors, a failure will occur when the handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the response,
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param attributes Attributes made available during rendering of the snippet
* @param descriptors The descriptions of the response's fields
* @return the handler
* @see #fieldWithPath(String)
*/
public static Snippet responseFields(Map<String, Object> attributes,
FieldDescriptor... descriptors) {
return new ResponseFieldsSnippet(attributes, Arrays.asList(descriptors));
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.payload;
/**
* Thrown to indicate that a failure has occurred during payload handling
*
* @author Andy Wilkinson
*
*/
@SuppressWarnings("serial")
class PayloadHandlingException extends RuntimeException {
/**
* Creates a new {@code PayloadHandlingException} with the given cause
* @param cause the cause of the failure
*/
PayloadHandlingException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.payload;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.Snippet;
/**
* A {@link Snippet} that documents the fields in a request.
*
* @author Andy Wilkinson
*/
class RequestFieldsSnippet extends AbstractFieldsSnippet {
RequestFieldsSnippet(List<FieldDescriptor> descriptors) {
this(null, descriptors);
}
RequestFieldsSnippet(Map<String, Object> attributes, List<FieldDescriptor> descriptors) {
super("request", attributes, descriptors);
}
@Override
protected MediaType getContentType(Operation operation) {
return operation.getRequest().getHeaders().getContentType();
}
@Override
protected byte[] getContent(Operation operation) throws IOException {
return operation.getRequest().getContent();
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.payload;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.Snippet;
/**
* A {@link Snippet} that documents the fields in a response.
*
* @author Andy Wilkinson
*/
class ResponseFieldsSnippet extends AbstractFieldsSnippet {
ResponseFieldsSnippet(List<FieldDescriptor> descriptors) {
this(null, descriptors);
}
ResponseFieldsSnippet(Map<String, Object> attributes,
List<FieldDescriptor> descriptors) {
super("response", attributes, descriptors);
}
@Override
protected MediaType getContentType(Operation operation) {
return operation.getResponse().getHeaders().getContentType();
}
@Override
protected byte[] getContent(Operation operation) throws IOException {
return operation.getResponse().getContent();
}
}

View File

@@ -0,0 +1,157 @@
/*
* 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.payload;
import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
/**
* A {@link ContentHandler} for XML content
*
* @author Andy Wilkinson
*/
class XmlContentHandler implements ContentHandler {
private final DocumentBuilder documentBuilder;
private final byte[] rawContent;
XmlContentHandler(byte[] rawContent) {
try {
this.documentBuilder = DocumentBuilderFactory.newInstance()
.newDocumentBuilder();
}
catch (ParserConfigurationException ex) {
throw new IllegalStateException("Failed to create document builder", ex);
}
this.rawContent = rawContent;
}
@Override
public List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors) {
List<FieldDescriptor> missingFields = new ArrayList<>();
Document payload = readPayload();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
if (!fieldDescriptor.isOptional()) {
NodeList matchingNodes = findMatchingNodes(fieldDescriptor, payload);
if (matchingNodes.getLength() == 0) {
missingFields.add(fieldDescriptor);
}
}
}
return missingFields;
}
private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, Document payload) {
try {
return (NodeList) createXPath(fieldDescriptor.getPath()).evaluate(payload,
XPathConstants.NODESET);
}
catch (XPathExpressionException ex) {
throw new PayloadHandlingException(ex);
}
}
private Document readPayload() {
try {
return this.documentBuilder.parse(new InputSource(new ByteArrayInputStream(
this.rawContent)));
}
catch (Exception ex) {
throw new PayloadHandlingException(ex);
}
}
private XPathExpression createXPath(String fieldPath) throws XPathExpressionException {
return XPathFactory.newInstance().newXPath().compile(fieldPath);
}
@Override
public String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors) {
Document payload = readPayload();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
NodeList matchingNodes;
try {
matchingNodes = (NodeList) createXPath(fieldDescriptor.getPath())
.evaluate(payload, XPathConstants.NODESET);
}
catch (XPathExpressionException ex) {
throw new PayloadHandlingException(ex);
}
for (int i = 0; i < matchingNodes.getLength(); i++) {
Node node = matchingNodes.item(i);
node.getParentNode().removeChild(node);
}
}
if (payload.getChildNodes().getLength() > 0) {
return prettyPrint(payload);
}
return null;
}
private String prettyPrint(Document document) {
try {
StringWriter stringWriter = new StringWriter();
StreamResult xmlOutput = new StreamResult(stringWriter);
TransformerFactory transformerFactory = TransformerFactory.newInstance();
transformerFactory.setAttribute("indent-number", 4);
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.transform(new DOMSource(document), xmlOutput);
return xmlOutput.getWriter().toString();
}
catch (Exception ex) {
throw new PayloadHandlingException(ex);
}
}
@Override
public Object determineFieldType(String path) {
try {
return new JsonFieldTypeResolver().resolveFieldType(path, readPayload());
}
catch (FieldDoesNotExistException ex) {
String message = "Cannot determine the type of the field '" + path + "' as"
+ " it is not present in the payload. Please provide a type using"
+ " FieldDescriptor.type(Object type).";
throw new FieldTypeRequiredException(message);
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.request;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.util.Assert;
abstract class AbstractParametersSnippet extends TemplatedSnippet {
private final Map<String, ParameterDescriptor> descriptorsByName = new LinkedHashMap<>();
protected AbstractParametersSnippet(String snippetName,
Map<String, Object> attributes, List<ParameterDescriptor> descriptors) {
super(snippetName, attributes);
for (ParameterDescriptor descriptor : descriptors) {
Assert.hasText(descriptor.getName());
Assert.hasText(descriptor.getDescription());
this.descriptorsByName.put(descriptor.getName(), descriptor);
}
}
@Override
protected Map<String, Object> createModel(Operation operation) throws IOException {
verifyParameterDescriptors(operation);
Map<String, Object> model = new HashMap<>();
List<Map<String, Object>> parameters = new ArrayList<>();
for (Entry<String, ParameterDescriptor> entry : this.descriptorsByName.entrySet()) {
parameters.add(entry.getValue().toModel());
}
model.put("parameters", parameters);
return model;
}
protected void verifyParameterDescriptors(Operation operation) {
Set<String> actualParameters = extractActualParameters(operation);
Set<String> expectedParameters = this.descriptorsByName.keySet();
Set<String> undocumentedParameters = new HashSet<String>(actualParameters);
undocumentedParameters.removeAll(expectedParameters);
Set<String> missingParameters = new HashSet<String>(expectedParameters);
missingParameters.removeAll(actualParameters);
if (!undocumentedParameters.isEmpty() || !missingParameters.isEmpty()) {
verificationFailed(undocumentedParameters, missingParameters);
}
else {
Assert.isTrue(actualParameters.equals(expectedParameters));
}
}
protected abstract Set<String> extractActualParameters(Operation operation);
protected abstract void verificationFailed(Set<String> undocumentedParameters,
Set<String> missingParameters);
}

View File

@@ -0,0 +1,68 @@
/*
* 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.request;
import java.util.HashMap;
import java.util.Map;
import org.springframework.restdocs.snippet.AbstractDescriptor;
/**
* A descriptor of a request or path parameter
*
* @author Andy Wilkinson
* @see RequestDocumentation#parameterWithName
*
*/
public class ParameterDescriptor extends AbstractDescriptor<ParameterDescriptor> {
private final String name;
private String description;
ParameterDescriptor(String name) {
this.name = name;
}
/**
* Specifies the description of the parameter
*
* @param description The parameter's description
* @return {@code this}
*/
public ParameterDescriptor description(String description) {
this.description = description;
return this;
}
String getName() {
return this.name;
}
String getDescription() {
return this.description;
}
Map<String, Object> toModel() {
Map<String, Object> model = new HashMap<>();
model.put("name", this.name);
model.put("description", this.description);
model.putAll(getAttributes());
return model;
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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.request;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.util.Assert;
/**
* A {@link Snippet} that documents the path parameters supported by a RESTful resource.
*
* @author Andy Wilkinson
*/
class PathParametersSnippet extends AbstractParametersSnippet {
private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
PathParametersSnippet(List<ParameterDescriptor> descriptors) {
this(null, descriptors);
}
PathParametersSnippet(Map<String, Object> attributes,
List<ParameterDescriptor> descriptors) {
super("path-parameters", attributes, descriptors);
}
@Override
protected Map<String, Object> createModel(Operation operation) throws IOException {
Map<String, Object> model = super.createModel(operation);
model.put("path", remoteQueryStringIfPresent(extractUrlTemplate(operation)));
return model;
}
private String remoteQueryStringIfPresent(String urlTemplate) {
int index = urlTemplate.indexOf('?');
if (index == -1) {
return urlTemplate;
}
return urlTemplate.substring(0, index);
}
@Override
protected Set<String> extractActualParameters(Operation operation) {
String urlTemplate = extractUrlTemplate(operation);
Matcher matcher = NAMES_PATTERN.matcher(urlTemplate);
Set<String> actualParameters = new HashSet<>();
while (matcher.find()) {
String match = matcher.group(1);
actualParameters.add(getParameterName(match));
}
return actualParameters;
}
private String extractUrlTemplate(Operation operation) {
String urlTemplate = (String) operation.getAttributes().get(
"org.springframework.restdocs.urlTemplate");
Assert.notNull(urlTemplate,
"urlTemplate not found. Did you use RestDocumentationRequestBuilders to "
+ "build the request?");
return urlTemplate;
}
private static String getParameterName(String match) {
int colonIndex = match.indexOf(':');
return colonIndex != -1 ? match.substring(0, colonIndex) : match;
}
@Override
protected void verificationFailed(Set<String> undocumentedParameters,
Set<String> missingParameters) {
String message = "";
if (!undocumentedParameters.isEmpty()) {
message += "Path parameters with the following names were not documented: "
+ undocumentedParameters;
}
if (!missingParameters.isEmpty()) {
if (message.length() > 0) {
message += ". ";
}
message += "Path parameters with the following names were not found in "
+ "the request: " + missingParameters;
}
throw new SnippetException(message);
}
}

View File

@@ -0,0 +1,106 @@
/*
* 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.request;
import java.util.Arrays;
import java.util.Map;
import javax.servlet.ServletRequest;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
/**
* Static factory methods for documenting aspects of a request sent to a RESTful API.
*
* @author Andy Wilkinson
*/
public abstract class RequestDocumentation {
private RequestDocumentation() {
}
/**
* Creates a {@link ParameterDescriptor} that describes a request or path parameter
* with the given {@code name}.
*
* @param name The name of the parameter
* @return a {@link ParameterDescriptor} ready for further configuration
*/
public static ParameterDescriptor parameterWithName(String name) {
return new ParameterDescriptor(name);
}
/**
* Returns a snippet that will document the path parameters from the API call's
* request.
*
* @param descriptors The descriptions of the parameters in the request's path
* @return the snippet
* @see PathVariable
*/
public static Snippet pathParameters(ParameterDescriptor... descriptors) {
return new PathParametersSnippet(Arrays.asList(descriptors));
}
/**
* Returns a snippet that will document the path parameters from the API call's
* request. The given {@code attributes} will be available during snippet rendering.
*
* @param attributes Attributes made available during rendering of the path parameters
* snippet
* @param descriptors The descriptions of the parameters in the request's path
* @return the snippet
* @see PathVariable
*/
public static Snippet pathParameters(Map<String, Object> attributes,
ParameterDescriptor... descriptors) {
return new PathParametersSnippet(attributes, Arrays.asList(descriptors));
}
/**
* Returns a snippet that will document the request parameters from the API call's
* request.
*
* @param descriptors The descriptions of the request's parameters
* @return the snippet
* @see RequestParam
* @see ServletRequest#getParameterMap()
*/
public static Snippet requestParameters(ParameterDescriptor... descriptors) {
return new RequestParametersSnippet(Arrays.asList(descriptors));
}
/**
* Returns a snippet that will document the request parameters from the API call's
* request. The given {@code attributes} will be available during snippet rendering.
*
* @param attributes Attributes made available during rendering of the request
* parameters snippet
* @param descriptors The descriptions of the request's parameters
* @return the snippet
* @see RequestParam
* @see ServletRequest#getParameterMap()
*/
public static Snippet requestParameters(Map<String, Object> attributes,
ParameterDescriptor... descriptors) {
return new RequestParametersSnippet(attributes, Arrays.asList(descriptors));
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.request;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletRequest;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.web.bind.annotation.RequestParam;
/**
* A {@link Snippet} that documents the request parameters supported by a RESTful
* resource.
* <p>
* Request parameters are sent as part of the query string or as posted from data.
*
* @author Andy Wilkinson
* @see ServletRequest#getParameterMap()
* @see RequestParam
*/
class RequestParametersSnippet extends AbstractParametersSnippet {
RequestParametersSnippet(List<ParameterDescriptor> descriptors) {
this(null, descriptors);
}
RequestParametersSnippet(Map<String, Object> attributes,
List<ParameterDescriptor> descriptors) {
super("request-parameters", attributes, descriptors);
}
@Override
protected void verificationFailed(Set<String> undocumentedParameters,
Set<String> missingParameters) {
String message = "";
if (!undocumentedParameters.isEmpty()) {
message += "Request parameters with the following names were not documented: "
+ undocumentedParameters;
}
if (!missingParameters.isEmpty()) {
if (message.length() > 0) {
message += ". ";
}
message += "Request parameters with the following names were not found in the request: "
+ missingParameters;
}
throw new SnippetException(message);
}
@Override
protected Set<String> extractActualParameters(Operation operation) {
return operation.getRequest().getParameters().keySet();
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.snippet;
import java.util.HashMap;
import java.util.Map;
import org.springframework.restdocs.snippet.Attributes.Attribute;
/**
* Base class for descriptors. Provides the ability to associate arbitrary attributes with
* a descriptor.
*
* @author Andy Wilkinson
*
* @param <T> the type of the descriptor
*/
public abstract class AbstractDescriptor<T extends AbstractDescriptor<T>> {
private Map<String, Object> attributes = new HashMap<>();
/**
* Sets the descriptor's attributes
*
* @param attributes the attributes
* @return the descriptor
*/
@SuppressWarnings("unchecked")
public T attributes(Attribute... attributes) {
for (Attribute attribute : attributes) {
this.attributes.put(attribute.getKey(), attribute.getValue());
}
return (T) this;
}
/**
* Returns the descriptor's attributes
*
* @return the attributes
*/
protected Map<String, Object> getAttributes() {
return this.attributes;
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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.snippet;
import java.util.HashMap;
import java.util.Map;
/**
* A fluent API for building a map of attributes
*
* @author Andy Wilkinson
*/
public abstract class Attributes {
private Attributes() {
}
/**
* Creates an attribute with the given {@code key}. A value for the attribute must
* still be specified.
*
* @param key The key of the attribute
* @return An {@code AttributeBuilder} to use to specify the value of the attribute
* @see AttributeBuilder#value(Object)
*/
public static AttributeBuilder key(String key) {
return new AttributeBuilder(key);
}
/**
* Creates a {@code Map} of the given {@code attributes}.
*
* @param attributes The attributes
* @return A Map of the attributes
*/
public static Map<String, Object> attributes(Attribute... attributes) {
Map<String, Object> attributeMap = new HashMap<>();
for (Attribute attribute : attributes) {
attributeMap.put(attribute.getKey(), attribute.getValue());
}
return attributeMap;
}
/**
* A simple builder for an attribute (key-value pair)
*/
public static class AttributeBuilder {
private final String key;
private AttributeBuilder(String key) {
this.key = key;
}
/**
* Configures the value of the attribute
*
* @param value The attribute's value
* @return A newly created {@code Attribute}
*/
public Attribute value(Object value) {
return new Attribute(this.key, value);
}
}
/**
* An attribute (key-value pair).
*/
public static class Attribute {
private final String key;
private final Object value;
/**
* Creates a new attribute with the given {@code key} and {@code value}.
* @param key the key
* @param value the value
*/
public Attribute(String key, Object value) {
this.key = key;
this.value = value;
}
/**
* Returns the attribute's key
* @return the key
*/
public String getKey() {
return this.key;
}
/**
* Returns the attribute's value
* @return the value
*/
public Object getValue() {
return this.value;
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.snippet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.restdocs.RestDocumentationContext;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
/**
* A {@link PlaceholderResolver} that resolves placeholders using a
* {@link RestDocumentationContext}. The following placeholders are supported:
* <ul>
* <li>{@code step} the {@link RestDocumentationContext#getStepCount() step current
* count}.
* <li>{@code methodName} - the name of the
* {@link RestDocumentationContext#getTestMethodName() current test method} formatted
* using camelCase
* <li>{@code method-name} - the name of the
* {@link RestDocumentationContext#getTestMethodName() current test method} formatted
* using kebab-case
* <li>{@code method_name} - the name of the
* {@link RestDocumentationContext#getTestMethodName() current test method} formatted
* using snake_case
* </ul>
*
* @author Andy Wilkinson
*/
public class RestDocumentationContextPlaceholderResolver implements PlaceholderResolver {
private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([A-Z])");
private final RestDocumentationContext context;
/**
* Creates a new placeholder resolver that will resolve placeholders using the given
* {@code context}.
*
* @param context the context to use
*/
public RestDocumentationContextPlaceholderResolver(RestDocumentationContext context) {
this.context = context;
}
@Override
public String resolvePlaceholder(String placeholderName) {
if ("step".equals(placeholderName)) {
return Integer.toString(this.context.getStepCount());
}
if ("methodName".equals(placeholderName)) {
return this.context.getTestMethodName();
}
if ("method-name".equals(placeholderName)) {
return camelCaseToDash(this.context.getTestMethodName());
}
if ("method_name".equals(placeholderName)) {
return camelCaseToUnderscore(this.context.getTestMethodName());
}
return null;
}
private String camelCaseToDash(String string) {
return camelCaseToSeparator(string, "-");
}
private String camelCaseToUnderscore(String string) {
return camelCaseToSeparator(string, "_");
}
private String camelCaseToSeparator(String string, String separator) {
Matcher matcher = CAMEL_CASE_PATTERN.matcher(string);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(result, separator + matcher.group(1).toLowerCase());
}
matcher.appendTail(result);
return result.toString();
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.snippet;
import java.io.IOException;
import org.springframework.restdocs.operation.Operation;
/**
* A {@link Snippet} is used to document aspects of a call to a RESTful API.
*
* @author Andy Wilkinson
*/
public interface Snippet {
/**
* Documents the call to the RESTful API described by the given {@code operation}.
*
* @param operation the API operation
* @throws IOException if a failure occurs will documenting the operation
*/
void document(Operation operation) throws IOException;
}

View File

@@ -0,0 +1,36 @@
/*
* 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.snippet;
/**
* A {@link RuntimeException} thrown to indicate a problem with the generation of a
* documentation snippet.
*
* @author Andy Wilkinson
*/
@SuppressWarnings("serial")
public class SnippetException extends RuntimeException {
/**
* Creates a new {@code SnippetException} described by the given {@code message}
* @param message the message that describes the problem
*/
public SnippetException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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.snippet;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import org.springframework.restdocs.RestDocumentationContext;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
/**
* Standard implementation of {@link WriterResolver}.
*
* @author Andy Wilkinson
*/
public class StandardWriterResolver implements WriterResolver {
private String encoding = "UTF-8";
private final PlaceholderResolver placeholderResolver;
private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper(
"{", "}");
/**
* Creates a new {@code StandardWriterResolver} that will use the given
* {@code placeholderResolver} to resolve any placeholders in the
* {@code operationName}.
*
* @param placeholderResolver the placeholder resolver
*/
public StandardWriterResolver(PlaceholderResolver placeholderResolver) {
this.placeholderResolver = placeholderResolver;
}
@Override
public Writer resolve(String operationName, String snippetName,
RestDocumentationContext context) throws IOException {
File outputFile = resolveFile(this.propertyPlaceholderHelper.replacePlaceholders(
operationName, this.placeholderResolver), snippetName + ".adoc", context);
if (outputFile != null) {
createDirectoriesIfNecessary(outputFile);
return new OutputStreamWriter(new FileOutputStream(outputFile), this.encoding);
}
else {
return new OutputStreamWriter(System.out, this.encoding);
}
}
@Override
public void setEncoding(String encoding) {
this.encoding = encoding;
}
protected File resolveFile(String outputDirectory, String fileName,
RestDocumentationContext context) {
File outputFile = new File(outputDirectory, fileName);
if (!outputFile.isAbsolute()) {
outputFile = makeRelativeToConfiguredOutputDir(outputFile, context);
}
return outputFile;
}
private File makeRelativeToConfiguredOutputDir(File outputFile,
RestDocumentationContext context) {
File configuredOutputDir = context.getOutputDirectory();
if (configuredOutputDir != null) {
return new File(configuredOutputDir, outputFile.getPath());
}
return null;
}
private void createDirectoriesIfNecessary(File outputFile) {
File parent = outputFile.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
throw new IllegalStateException("Failed to create directory '" + parent + "'");
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.snippet;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import org.springframework.restdocs.RestDocumentationContext;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.templates.Template;
import org.springframework.restdocs.templates.TemplateEngine;
/**
* Base class for a {@link Snippet} that is produced using a {@link Template} and
* {@link TemplateEngine}.
*
* @author Andy Wilkinson
*/
public abstract class TemplatedSnippet implements Snippet {
private final Map<String, Object> attributes = new HashMap<>();
private final String snippetName;
protected TemplatedSnippet(String snippetName, Map<String, Object> attributes) {
this.snippetName = snippetName;
if (attributes != null) {
this.attributes.putAll(attributes);
}
}
@Override
public void document(Operation operation) throws IOException {
RestDocumentationContext context = (RestDocumentationContext) operation
.getAttributes().get(RestDocumentationContext.class.getName());
WriterResolver writerResolver = (WriterResolver) operation.getAttributes().get(
WriterResolver.class.getName());
try (Writer writer = writerResolver.resolve(operation.getName(),
this.snippetName, context)) {
Map<String, Object> model = createModel(operation);
model.putAll(this.attributes);
TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes()
.get(TemplateEngine.class.getName());
writer.append(templateEngine.compileTemplate(this.snippetName).render(model));
}
}
protected abstract Map<String, Object> createModel(Operation operation)
throws IOException;
}

View File

@@ -0,0 +1,51 @@
/*
* 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.snippet;
import java.io.IOException;
import java.io.Writer;
import org.springframework.restdocs.RestDocumentationContext;
/**
* A {@code WriterResolver} is used to access the {@link Writer} that should be used to
* write a snippet for an operation that is being documented.
*
* @author Andy Wilkinson
*/
public interface WriterResolver {
/**
* Returns a writer that can be used to write the snippet with the given name for the
* operation with the given name.
* @param operationName the name of the operation that is being documented
* @param snippetName the name of the snippet
* @param restDocumentationContext the current documentation context
* @return the writer
* @throws IOException if a writer cannot be resolved
*/
Writer resolve(String operationName, String snippetName,
RestDocumentationContext restDocumentationContext) throws IOException;
/**
* Configures the encoding that should be used by any writers produced by this
* resolver
* @param encoding the encoding
*/
void setEncoding(String encoding);
}

View File

@@ -0,0 +1,49 @@
/*
* 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.templates;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
/**
* Standard implementation of {@link TemplateResourceResolver}.
* <p>
* Templates are resolved by first looking for a resource on the classpath named
* {@code org/springframework/restdocs/templates/&#123;name&#125;.snippet}. If no such
* resource exists {@code default-} is prepended to the name and the classpath is checked
* again. The built-in snippet templates are all named {@code default- name}, thereby
* allowing them to be overridden.
*
* @author Andy Wilkinson
*/
public class StandardTemplateResourceResolver implements TemplateResourceResolver {
@Override
public Resource resolveTemplateResource(String name) {
ClassPathResource classPathResource = new ClassPathResource(
"org/springframework/restdocs/templates/" + name + ".snippet");
if (!classPathResource.exists()) {
classPathResource = new ClassPathResource(
"org/springframework/restdocs/templates/default-" + name + ".snippet");
if (!classPathResource.exists()) {
throw new IllegalStateException("Template named '" + name
+ "' could not be resolved");
}
}
return classPathResource;
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.templates;
import java.util.Map;
/**
* A compiled {@code Template} that can be rendered to a {@link String}.
*
* @author Andy Wilkinson
*
*/
public interface Template {
/**
* Renders the template to a {@link String} using the given {@code context} for
* variable/property resolution.
*
* @param context The context to use
* @return The rendered template
*/
String render(Map<String, Object> context);
}

View File

@@ -0,0 +1,42 @@
/*
* 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.templates;
import java.io.IOException;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
/**
* A {@code TemplateEngine} is used to render documentation snippets.
*
* @author Andy Wilkinson
* @see MustacheTemplateEngine
*/
public interface TemplateEngine {
/**
* Compiles the template at the given {@code path}. Typically, a
* {@link TemplateResourceResolver} will be used to resolve the path into a resource
* that can be read and compiled.
*
* @param path the path of the template
* @return the compiled {@code Template}
* @throws IOException if compilation fails
*/
Template compileTemplate(String path) throws IOException;
}

View File

@@ -0,0 +1,37 @@
/*
* 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.templates;
import org.springframework.core.io.Resource;
/**
* A {@code TemplateResourceResolver} is responsible for resolving a name for a template
* into a {@link Resource} from which the template can be read.
*
* @author Andy Wilkinson
*/
public interface TemplateResourceResolver {
/**
* Resolves a {@link Resource} for the template with the given {@code name}.
*
* @param name the name of the template
* @return the {@code Resource} from which the template can be read
*/
public Resource resolveTemplateResource(String name);
}

View File

@@ -0,0 +1,47 @@
/*
* 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.templates.mustache;
import java.util.Map;
import org.springframework.restdocs.templates.Template;
/**
* An adapter that exposes a compiled <a href="https://mustache.github.io">Mustache</a>
* template as a {@link Template}.
*
* @author Andy Wilkinson
*/
public class MustacheTemplate implements Template {
private final org.springframework.restdocs.mustache.Template delegate;
/**
* Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. When
* rendered, the given {@code defaultContext} will be combined with the render context
* prior to executing the delegate.
* @param delegate The delegate to adapt
*/
public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate) {
this.delegate = delegate;
}
@Override
public String render(Map<String, Object> context) {
return this.delegate.execute(context);
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.templates.mustache;
import java.io.IOException;
import java.io.InputStreamReader;
import org.springframework.core.io.Resource;
import org.springframework.restdocs.mustache.Mustache;
import org.springframework.restdocs.mustache.Mustache.Compiler;
import org.springframework.restdocs.templates.Template;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
/**
* A <a href="https://mustache.github.io">Mustache</a>-based {@link TemplateEngine}
* implemented using <a href="https://github.com/samskivert/jmustache">JMustache</a>.
* <p>
* Note that JMustache has been repackaged and embedded to prevent classpath conflicts.
*
* @author Andy Wilkinson
*/
public class MustacheTemplateEngine implements TemplateEngine {
private final Compiler compiler = Mustache.compiler().escapeHTML(false);
private final TemplateResourceResolver templateResourceResolver;
/**
* Creates a new {@link MustacheTemplateEngine} that will use the given
* {@code templateResourceResolver} to resolve template paths.
*
* @param templateResourceResolver The resolve to use
*/
public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) {
this.templateResourceResolver = templateResourceResolver;
}
@Override
public Template compileTemplate(String name) throws IOException {
Resource templateResource = this.templateResourceResolver
.resolveTemplateResource(name);
return new MustacheTemplate(this.compiler.compile(new InputStreamReader(
templateResource.getInputStream())));
}
}

View File

@@ -0,0 +1,13 @@
javax.validation.constraints.AssertFalse.description=Must be false
javax.validation.constraints.AssertTrue.description=Must be true
javax.validation.constraints.DecimalMax.description=Must be at most ${value}
javax.validation.constraints.DecimalMin.description=Must be at least ${value}
javax.validation.constraints.Digits.description=Must have at most ${integer} integral digits and ${fraction} fractional digits
javax.validation.constraints.Future.description=Must be in the future
javax.validation.constraints.Max.description=Must be at most ${value}
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.Size.description=Size must be between ${min} and ${max} inclusive

View File

@@ -0,0 +1,4 @@
[source,bash]
----
$ curl {{arguments}}
----

View File

@@ -0,0 +1,8 @@
[source,http]
----
{{method}} {{path}} HTTP/1.1
{{#headers}}
{{name}}: {{value}}
{{/headers}}
{{requestBody}}
----

View File

@@ -0,0 +1,8 @@
[source,http]
----
HTTP/1.1 {{statusCode}} {{statusReason}}
{{#headers}}
{{name}}: {{value}}
{{/headers}}
{{responseBody}}
----

View File

@@ -0,0 +1,9 @@
|===
|Relation|Description
{{#links}}
|{{rel}}
|{{description}}
{{/links}}
|===

View File

@@ -0,0 +1,10 @@
.{{path}}
|===
|Parameter|Description
{{#parameters}}
|{{name}}
|{{description}}
{{/parameters}}
|===

View File

@@ -0,0 +1,10 @@
|===
|Path|Type|Description
{{#fields}}
|{{path}}
|{{type}}
|{{description}}
{{/fields}}
|===

View File

@@ -0,0 +1,9 @@
|===
|Parameter|Description
{{#parameters}}
|{{name}}
|{{description}}
{{/parameters}}
|===

View File

@@ -0,0 +1,10 @@
|===
|Path|Type|Description
{{#fields}}
|{{path}}
|{{type}}
|{{description}}
{{/fields}}
|===

View File

@@ -0,0 +1,198 @@
/*
* 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.constraints;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
import java.math.BigDecimal;
import java.util.Date;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.junit.Test;
/**
* Tests for {@link ConstraintDescriptions}
*
* @author Andy Wilkinson
*/
public class ConstraintDescriptionsTests {
private final ConstraintDescriptions constraintDescriptions = new ConstraintDescriptions(
Constrained.class);
@Test
public void assertFalse() {
assertThat(this.constraintDescriptions.descriptionsForProperty("assertFalse"),
contains("Must be false"));
}
@Test
public void assertTrue() {
assertThat(this.constraintDescriptions.descriptionsForProperty("assertTrue"),
contains("Must be true"));
}
@Test
public void decimalMax() {
assertThat(this.constraintDescriptions.descriptionsForProperty("decimalMax"),
contains("Must be at most 9.875"));
}
@Test
public void decimalMin() {
assertThat(this.constraintDescriptions.descriptionsForProperty("decimalMin"),
contains("Must be at least 1.5"));
}
@Test
public void digits() {
assertThat(this.constraintDescriptions.descriptionsForProperty("digits"),
contains("Must have at most 2 integral digits and 5 fractional digits"));
}
@Test
public void future() {
assertThat(this.constraintDescriptions.descriptionsForProperty("future"),
contains("Must be in the future"));
}
@Test
public void max() {
assertThat(this.constraintDescriptions.descriptionsForProperty("max"),
contains("Must be at most 10"));
}
@Test
public void min() {
assertThat(this.constraintDescriptions.descriptionsForProperty("min"),
contains("Must be at least 5"));
}
@Test
public void notNull() {
assertThat(this.constraintDescriptions.descriptionsForProperty("notNull"),
contains("Must not be null"));
}
@Test
public void nul() {
assertThat(this.constraintDescriptions.descriptionsForProperty("nul"),
contains("Must be null"));
}
@Test
public void past() {
assertThat(this.constraintDescriptions.descriptionsForProperty("past"),
contains("Must be in the past"));
}
@Test
public void pattern() {
assertThat(this.constraintDescriptions.descriptionsForProperty("pattern"),
contains("Must match the regular expression '[A-Z][a-z]+'"));
}
@Test
public void size() {
assertThat(this.constraintDescriptions.descriptionsForProperty("size"),
contains("Size must be between 0 and 10 inclusive"));
}
@Test
public void sizeList() {
assertThat(
this.constraintDescriptions.descriptionsForProperty("sizeList"),
contains("Size must be between 1 and 4 inclusive",
"Size must be between 8 and 10 inclusive"));
}
@Test
public void unconstrained() {
assertThat(this.constraintDescriptions.descriptionsForProperty("unconstrained"),
hasSize(0));
}
@Test
public void nonExistentProperty() {
assertThat(this.constraintDescriptions.descriptionsForProperty("doesNotExist"),
hasSize(0));
}
private static class Constrained {
@AssertFalse
private boolean assertFalse;
@AssertTrue
private boolean assertTrue;
@DecimalMax("9.875")
private BigDecimal decimalMax;
@DecimalMin("1.5")
private BigDecimal decimalMin;
@Digits(fraction = 5, integer = 2)
private BigDecimal digits;
@Future
private Date future;
@NotNull
private String notNull;
@Max(10)
private int max;
@Min(5)
private int min;
@Null
private String nul;
@Past
private Date past;
@Pattern(regexp = "[A-Z][a-z]+")
private String pattern;
@Size(min = 0, max = 10)
private String size;
@Size.List({ @Size(min = 1, max = 4), @Size(min = 8, max = 10) })
private String sizeList;
@SuppressWarnings("unused")
private String unconstrained;
}
}

View File

@@ -0,0 +1,208 @@
/*
* 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.constraints;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.ListResourceBundle;
import java.util.Map;
import java.util.ResourceBundle;
import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.junit.Test;
/**
* Tests for {@link ResourceBundleConstraintDescriptionResolver}
*
* @author Andy Wilkinson
*/
public class ResourceBundleConstraintDescriptionResolverTests {
private final ResourceBundleConstraintDescriptionResolver resolver = new ResourceBundleConstraintDescriptionResolver();
@Test
public void defaultMessageAssertFalse() {
String description = this.resolver.resolveDescription(new Constraint(
AssertFalse.class.getName(), Collections.<String, Object> emptyMap()));
assertThat(description, is(equalTo("Must be false")));
}
@Test
public void defaultMessageAssertTrue() {
String description = this.resolver.resolveDescription(new Constraint(
AssertTrue.class.getName(), Collections.<String, Object> emptyMap()));
assertThat(description, is(equalTo("Must be true")));
}
@Test
public void defaultMessageDecimalMax() {
Map<String, Object> configuration = new HashMap<>();
configuration.put("value", "9.875");
String description = this.resolver.resolveDescription(new Constraint(
DecimalMax.class.getName(), configuration));
assertThat(description, is(equalTo("Must be at most 9.875")));
}
@Test
public void defaultMessageDecimalMin() {
Map<String, Object> configuration = new HashMap<>();
configuration.put("value", "1.5");
String description = this.resolver.resolveDescription(new Constraint(
DecimalMin.class.getName(), configuration));
assertThat(description, is(equalTo("Must be at least 1.5")));
}
@Test
public void defaultMessageDigits() {
Map<String, Object> configuration = new HashMap<>();
configuration.put("integer", "2");
configuration.put("fraction", "5");
String description = this.resolver.resolveDescription(new Constraint(Digits.class
.getName(), configuration));
assertThat(description, is(equalTo("Must have at most 2 integral digits and 5 "
+ "fractional digits")));
}
@Test
public void defaultMessageFuture() {
String description = this.resolver.resolveDescription(new Constraint(Future.class
.getName(), Collections.<String, Object> emptyMap()));
assertThat(description, is(equalTo("Must be in the future")));
}
@Test
public void defaultMessageMax() {
Map<String, Object> configuration = new HashMap<>();
configuration.put("value", 10);
String description = this.resolver.resolveDescription(new Constraint(Max.class
.getName(), configuration));
assertThat(description, is(equalTo("Must be at most 10")));
}
@Test
public void defaultMessageMin() {
Map<String, Object> configuration = new HashMap<>();
configuration.put("value", 10);
String description = this.resolver.resolveDescription(new Constraint(Min.class
.getName(), configuration));
assertThat(description, is(equalTo("Must be at least 10")));
}
@Test
public void defaultMessageNotNull() {
String description = this.resolver.resolveDescription(new Constraint(
NotNull.class.getName(), Collections.<String, Object> emptyMap()));
assertThat(description, is(equalTo("Must not be null")));
}
@Test
public void defaultMessageNull() {
String description = this.resolver.resolveDescription(new Constraint(Null.class
.getName(), Collections.<String, Object> emptyMap()));
assertThat(description, is(equalTo("Must be null")));
}
@Test
public void defaultMessagePast() {
String description = this.resolver.resolveDescription(new Constraint(Past.class
.getName(), Collections.<String, Object> emptyMap()));
assertThat(description, is(equalTo("Must be in the past")));
}
@Test
public void defaultMessagePattern() {
Map<String, Object> configuration = new HashMap<>();
configuration.put("regexp", "[A-Z][a-z]+");
String description = this.resolver.resolveDescription(new Constraint(
Pattern.class.getName(), configuration));
assertThat(description,
is(equalTo("Must match the regular expression '[A-Z][a-z]+'")));
}
@Test
public void defaultMessageSize() {
Map<String, Object> configuration = new HashMap<>();
configuration.put("min", 2);
configuration.put("max", 10);
String description = this.resolver.resolveDescription(new Constraint(Size.class
.getName(), configuration));
assertThat(description, is(equalTo("Size must be between 2 and 10 inclusive")));
}
@Test
public void customMessage() {
Thread.currentThread().setContextClassLoader(new ClassLoader() {
@Override
public URL getResource(String name) {
if (name.startsWith("org/springframework/restdocs/constraints/ConstraintDescriptions")) {
return super
.getResource("org/springframework/restdocs/constraints/TestConstraintDescriptions.properties");
}
return super.getResource(name);
}
});
try {
String description = new ResourceBundleConstraintDescriptionResolver()
.resolveDescription(new Constraint(NotNull.class.getName(),
Collections.<String, Object> emptyMap()));
assertThat(description, is(equalTo("Should not be null")));
}
finally {
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
}
}
@Test
public void customResourceBundle() {
ResourceBundle bundle = new ListResourceBundle() {
@Override
protected Object[][] getContents() {
return new String[][] { { NotNull.class.getName() + ".description",
"Not null" } };
}
};
String description = new ResourceBundleConstraintDescriptionResolver(bundle)
.resolveDescription(new Constraint(NotNull.class.getName(), Collections
.<String, Object> emptyMap()));
assertThat(description, is(equalTo("Not null")));
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.constraints;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.validation.Payload;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Size;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hibernate.validator.constraints.CompositionType;
import org.hibernate.validator.constraints.ConstraintComposition;
import org.hibernate.validator.constraints.NotBlank;
import org.junit.Test;
/**
* Tests for {@link ValidatorConstraintResolver}
*
* @author Andy Wilkinson
*/
public class ValidatorConstraintResolverTests {
private final ValidatorConstraintResolver resolver = new ValidatorConstraintResolver();
@Test
public void singleFieldConstraint() {
List<Constraint> constraints = this.resolver.resolveForProperty("single",
ConstrainedFields.class);
assertThat(constraints, hasSize(1));
assertThat(constraints.get(0).getName(), is(NotNull.class.getName()));
}
@SuppressWarnings("unchecked")
@Test
public void multipleFieldConstraints() {
List<Constraint> constraints = this.resolver.resolveForProperty("multiple",
ConstrainedFields.class);
assertThat(constraints, hasSize(2));
assertThat(
constraints,
containsInAnyOrder(constraint(NotNull.class), constraint(Size.class)
.config("min", 8).config("max", 16)));
}
@Test
public void noFieldConstraints() {
List<Constraint> constraints = this.resolver.resolveForProperty("none",
ConstrainedFields.class);
assertThat(constraints, hasSize(0));
}
@Test
public void compositeConstraint() {
List<Constraint> constraints = this.resolver.resolveForProperty("composite",
ConstrainedFields.class);
assertThat(constraints, hasSize(1));
}
private static class ConstrainedFields {
@NotNull
private String single;
@NotNull
@Size(min = 8, max = 16)
private String multiple;
@SuppressWarnings("unused")
private String none;
@CompositeConstraint
private String composite;
}
@ConstraintComposition(CompositionType.OR)
@Null
@NotBlank
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@javax.validation.Constraint(validatedBy = {})
public @interface CompositeConstraint {
String message() default "Must be null or not blank";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
private ConstraintMatcher constraint(final Class<? extends Annotation> annotation) {
return new ConstraintMatcher(annotation);
}
private static class ConstraintMatcher extends BaseMatcher<Constraint> {
private final Class<?> annotation;
private final Map<String, Object> configuration = new HashMap<>();
private ConstraintMatcher(Class<?> annotation) {
this.annotation = annotation;
}
public ConstraintMatcher config(String key, Object value) {
this.configuration.put(key, value);
return this;
}
@Override
public boolean matches(Object item) {
if (!(item instanceof Constraint)) {
return false;
}
Constraint constraint = (Constraint) item;
if (!constraint.getName().equals(this.annotation.getName())) {
return false;
}
for (Entry<String, Object> entry : this.configuration.entrySet()) {
if (!constraint.getConfiguration().get(entry.getKey())
.equals(entry.getValue())) {
return false;
}
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText("Constraint named " + this.annotation.getName()
+ " with configuration " + this.configuration);
}
}
}

View File

@@ -0,0 +1,260 @@
/*
* 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 static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.codeBlock;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
/**
* Tests for {@link CurlRequestSnippet}
*
* @author Andy Wilkinson
* @author Yann Le Guern
* @author Dmitriy Mayboroda
* @author Jonathan Pearlin
*/
public class CurlRequestSnippetTests {
@Rule
public ExpectedSnippet snippet = new ExpectedSnippet();
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void getRequest() throws IOException {
this.snippet.expectCurlRequest("get-request").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo' -i"));
new CurlRequestSnippet().document(new OperationBuilder("get-request",
this.snippet.getOutputDirectory()).request("http://localhost/foo")
.build());
}
@Test
public void nonGetRequest() throws IOException {
this.snippet.expectCurlRequest("non-get-request").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST"));
new CurlRequestSnippet().document(new OperationBuilder("non-get-request",
this.snippet.getOutputDirectory()).request("http://localhost/foo")
.method("POST").build());
}
@Test
public void requestWithContent() throws IOException {
this.snippet.expectCurlRequest("request-with-content").withContents(
codeBlock("bash")
.content("$ curl 'http://localhost/foo' -i -d 'content'"));
new CurlRequestSnippet().document(new OperationBuilder("request-with-content",
this.snippet.getOutputDirectory()).request("http://localhost/foo")
.content("content").build());
}
@Test
public void requestWithQueryString() throws IOException {
this.snippet.expectCurlRequest("request-with-query-string")
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo?param=value' -i"));
new CurlRequestSnippet().document(new OperationBuilder(
"request-with-query-string", this.snippet.getOutputDirectory()).request(
"http://localhost/foo?param=value").build());
}
@Test
public void postRequestWithOneParameter() throws IOException {
this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'"));
new CurlRequestSnippet()
.document(new OperationBuilder("post-request-with-one-parameter",
this.snippet.getOutputDirectory())
.request("http://localhost/foo").method("POST").param("k1", "v1")
.build());
}
@Test
public void postRequestWithMultipleParameters() throws IOException {
this.snippet.expectCurlRequest("post-request-with-multiple-parameters")
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST"
+ " -d 'k1=v1&k1=v1-bis&k2=v2'"));
new CurlRequestSnippet().document(new OperationBuilder(
"post-request-with-multiple-parameters", this.snippet
.getOutputDirectory()).request("http://localhost/foo")
.method("POST").param("k1", "v1", "v1-bis").param("k2", "v2").build());
}
@Test
public void postRequestWithUrlEncodedParameter() throws IOException {
this.snippet
.expectCurlRequest("post-request-with-url-encoded-parameter")
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'"));
new CurlRequestSnippet().document(new OperationBuilder(
"post-request-with-url-encoded-parameter", this.snippet
.getOutputDirectory()).request("http://localhost/foo")
.method("POST").param("k1", "a&b").build());
}
@Test
public void putRequestWithOneParameter() throws IOException {
this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'"));
new CurlRequestSnippet().document(new OperationBuilder(
"put-request-with-one-parameter", this.snippet.getOutputDirectory())
.request("http://localhost/foo").method("PUT").param("k1", "v1").build());
}
@Test
public void putRequestWithMultipleParameters() throws IOException {
this.snippet.expectCurlRequest("put-request-with-multiple-parameters")
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT"
+ " -d 'k1=v1&k1=v1-bis&k2=v2'"));
new CurlRequestSnippet()
.document(new OperationBuilder("put-request-with-multiple-parameters",
this.snippet.getOutputDirectory())
.request("http://localhost/foo").method("PUT").param("k1", "v1")
.param("k1", "v1-bis").param("k2", "v2").build());
}
@Test
public void putRequestWithUrlEncodedParameter() throws IOException {
this.snippet.expectCurlRequest("put-request-with-url-encoded-parameter")
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'"));
new CurlRequestSnippet().document(new OperationBuilder(
"put-request-with-url-encoded-parameter", this.snippet
.getOutputDirectory()).request("http://localhost/foo")
.method("PUT").param("k1", "a&b").build());
}
@Test
public void requestWithHeaders() throws IOException {
this.snippet.expectCurlRequest("request-with-headers").withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i"
+ " -H 'Content-Type: application/json' -H 'a: alpha'"));
new CurlRequestSnippet().document(new OperationBuilder("request-with-headers",
this.snippet.getOutputDirectory()).request("http://localhost/foo")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header("a", "alpha").build());
}
@Test
public void multipartPostWithNoSubmittedFileName() throws IOException {
String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H "
+ "'Content-Type: multipart/form-data' -F "
+ "'metadata={\"description\": \"foo\"}'";
this.snippet.expectCurlRequest("multipart-post-no-original-filename")
.withContents(codeBlock("bash").content(expectedContent));
new CurlRequestSnippet().document(new OperationBuilder(
"multipart-post-no-original-filename", this.snippet.getOutputDirectory())
.request("http://localhost/upload").method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("metadata", "{\"description\": \"foo\"}".getBytes()).build());
}
@Test
public void multipartPostWithContentType() throws IOException {
String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H "
+ "'Content-Type: multipart/form-data' -F "
+ "'image=@documents/images/example.png;type=image/png'";
this.snippet.expectCurlRequest("multipart-post-with-content-type").withContents(
codeBlock("bash").content(expectedContent));
new CurlRequestSnippet().document(new OperationBuilder(
"multipart-post-with-content-type", this.snippet.getOutputDirectory())
.request("http://localhost/upload").method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("image", new byte[0])
.header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE)
.submittedFileName("documents/images/example.png").build());
}
@Test
public void multipartPost() throws IOException {
String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H "
+ "'Content-Type: multipart/form-data' -F "
+ "'image=@documents/images/example.png'";
this.snippet.expectCurlRequest("multipart-post").withContents(
codeBlock("bash").content(expectedContent));
new CurlRequestSnippet().document(new OperationBuilder("multipart-post",
this.snippet.getOutputDirectory()).request("http://localhost/upload")
.method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("image", new byte[0])
.submittedFileName("documents/images/example.png").build());
}
@Test
public void multipartPostWithParameters() throws IOException {
String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H "
+ "'Content-Type: multipart/form-data' -F "
+ "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' "
+ "-F 'b=banana'";
this.snippet.expectCurlRequest("multipart-post-with-parameters").withContents(
codeBlock("bash").content(expectedContent));
new CurlRequestSnippet().document(new OperationBuilder(
"multipart-post-with-parameters", this.snippet.getOutputDirectory())
.request("http://localhost/upload").method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("image", new byte[0])
.submittedFileName("documents/images/example.png").and()
.param("a", "apple", "avocado").param("b", "banana").build());
}
@Test
public void customAttributes() throws IOException {
this.snippet.expectCurlRequest("custom-attributes").withContents(
containsString("curl request title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("curl-request"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/curl-request-with-title.snippet"));
new CurlRequestSnippet(attributes(key("title").value("curl request title")))
.document(new OperationBuilder("custom-attributes", this.snippet
.getOutputDirectory())
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))
.request("http://localhost/foo").build());
}
}

View File

@@ -0,0 +1,227 @@
/*
* 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.http;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.httpRequest;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
import static org.springframework.web.bind.annotation.RequestMethod.PUT;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
/**
* Tests for {@link HttpRequestSnippet}
*
* @author Andy Wilkinson
* @author Jonathan Pearlin
*
*/
public class HttpRequestSnippetTests {
private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
@Rule
public final ExpectedSnippet snippet = new ExpectedSnippet();
@Test
public void getRequest() throws IOException {
this.snippet.expectHttpRequest("get-request").withContents(
httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header(
"Alpha", "a"));
new HttpRequestSnippet().document(new OperationBuilder("get-request",
this.snippet.getOutputDirectory()).request("http://localhost/foo")
.header("Alpha", "a").build());
}
@Test
public void getRequestWithQueryString() throws IOException {
this.snippet.expectHttpRequest("get-request-with-query-string").withContents(
httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost"));
new HttpRequestSnippet().document(new OperationBuilder(
"get-request-with-query-string", this.snippet.getOutputDirectory())
.request("http://localhost/foo?bar=baz").build());
}
@Test
public void postRequestWithContent() throws IOException {
this.snippet.expectHttpRequest("post-request-with-content").withContents(
httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content(
"Hello, world"));
new HttpRequestSnippet().document(new OperationBuilder(
"post-request-with-content", this.snippet.getOutputDirectory())
.request("http://localhost/foo").method("POST").content("Hello, world")
.build());
}
@Test
public void postRequestWithParameter() throws IOException {
this.snippet.expectHttpRequest("post-request-with-parameter").withContents(
httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost")
.header("Content-Type", "application/x-www-form-urlencoded")
.content("b%26r=baz&a=alpha"));
new HttpRequestSnippet().document(new OperationBuilder(
"post-request-with-parameter", this.snippet.getOutputDirectory())
.request("http://localhost/foo").method("POST").param("b&r", "baz")
.param("a", "alpha").build());
}
@Test
public void putRequestWithContent() throws IOException {
this.snippet.expectHttpRequest("put-request-with-content").withContents(
httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content(
"Hello, world"));
new HttpRequestSnippet().document(new OperationBuilder(
"put-request-with-content", this.snippet.getOutputDirectory())
.request("http://localhost/foo").method("PUT").content("Hello, world")
.build());
}
@Test
public void putRequestWithParameter() throws IOException {
this.snippet.expectHttpRequest("put-request-with-parameter").withContents(
httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost")
.header("Content-Type", "application/x-www-form-urlencoded")
.content("b%26r=baz&a=alpha"));
new HttpRequestSnippet().document(new OperationBuilder(
"put-request-with-parameter", this.snippet.getOutputDirectory())
.request("http://localhost/foo").method("PUT").param("b&r", "baz")
.param("a", "alpha").build());
}
@Test
public void multipartPost() throws IOException {
String expectedContent = createPart(String.format("Content-Disposition: "
+ "form-data; " + "name=image%n%n<< data >>"));
this.snippet.expectHttpRequest("multipart-post").withContents(
httpRequest(POST, "/upload")
.header(HttpHeaders.HOST, "localhost")
.header("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY)
.content(expectedContent));
new HttpRequestSnippet().document(new OperationBuilder("multipart-post",
this.snippet.getOutputDirectory()).request("http://localhost/upload")
.method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("image", "<< data >>".getBytes()).build());
}
@Test
public void multipartPostWithParameters() throws IOException {
String param1Part = createPart(String.format("Content-Disposition: form-data; "
+ "name=a%n%napple"), false);
String param2Part = createPart(String.format("Content-Disposition: form-data; "
+ "name=a%n%navocado"), false);
String param3Part = createPart(String.format("Content-Disposition: form-data; "
+ "name=b%n%nbanana"), false);
String filePart = createPart(String.format("Content-Disposition: form-data; "
+ "name=image%n%n<< data >>"));
String expectedContent = param1Part + param2Part + param3Part + filePart;
this.snippet.expectHttpRequest("multipart-post-with-parameters").withContents(
httpRequest(POST, "/upload")
.header(HttpHeaders.HOST, "localhost")
.header("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY)
.content(expectedContent));
new HttpRequestSnippet().document(new OperationBuilder(
"multipart-post-with-parameters", this.snippet.getOutputDirectory())
.request("http://localhost/upload").method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.param("a", "apple", "avocado").param("b", "banana")
.part("image", "<< data >>".getBytes()).build());
}
@Test
public void multipartPostWithContentType() throws IOException {
String expectedContent = createPart(String
.format("Content-Disposition: form-data; name=image%nContent-Type: "
+ "image/png%n%n<< data >>"));
this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents(
httpRequest(POST, "/upload")
.header(HttpHeaders.HOST, "localhost")
.header("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY)
.content(expectedContent));
new HttpRequestSnippet().document(new OperationBuilder(
"multipart-post-with-content-type", this.snippet.getOutputDirectory())
.request("http://localhost/upload").method("POST")
.header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE)
.part("image", "<< data >>".getBytes())
.header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE).build());
}
@Test
public void getRequestWithCustomHost() throws IOException {
this.snippet.expectHttpRequest("get-request-custom-host").withContents(
httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com"));
new HttpRequestSnippet().document(new OperationBuilder("get-request-custom-host",
this.snippet.getOutputDirectory()).request("http://localhost/foo")
.header(HttpHeaders.HOST, "api.example.com").build());
}
@Test
public void requestWithCustomSnippetAttributes() throws IOException {
this.snippet.expectHttpRequest("request-with-snippet-attributes").withContents(
containsString("Title for the request"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("http-request"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/http-request-with-title.snippet"));
new HttpRequestSnippet(attributes(key("title").value("Title for the request")))
.document(new OperationBuilder("request-with-snippet-attributes",
this.snippet.getOutputDirectory())
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))
.request("http://localhost/foo").build());
}
private String createPart(String content) {
return this.createPart(content, true);
}
private String createPart(String content, boolean last) {
StringBuilder part = new StringBuilder();
part.append(String.format("--%s%n%s%n", BOUNDARY, content));
if (last) {
part.append(String.format("--%s--", BOUNDARY));
}
return part.toString();
}
}

View File

@@ -0,0 +1,104 @@
/*
* 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.http;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.httpResponse;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
/**
* Tests for {@link HttpResponseSnippet}
*
* @author Andy Wilkinson
* @author Jonathan Pearlin
*/
public class HttpResponseSnippetTests {
@Rule
public final ExpectedSnippet snippet = new ExpectedSnippet();
@Test
public void basicResponse() throws IOException {
this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK));
new HttpResponseSnippet().document(new OperationBuilder("basic-response",
this.snippet.getOutputDirectory()).build());
}
@Test
public void nonOkResponse() throws IOException {
this.snippet.expectHttpResponse("non-ok-response").withContents(
httpResponse(BAD_REQUEST));
new HttpResponseSnippet().document(new OperationBuilder("non-ok-response",
this.snippet.getOutputDirectory()).response().status(BAD_REQUEST.value())
.build());
}
@Test
public void responseWithHeaders() throws IOException {
this.snippet.expectHttpResponse("response-with-headers").withContents(
httpResponse(OK) //
.header("Content-Type", "application/json") //
.header("a", "alpha"));
new HttpResponseSnippet().document(new OperationBuilder("response-with-headers",
this.snippet.getOutputDirectory()).response()
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header("a", "alpha").build());
}
@Test
public void responseWithContent() throws IOException {
this.snippet.expectHttpResponse("response-with-content").withContents(
httpResponse(OK).content("content"));
new HttpResponseSnippet().document(new OperationBuilder("response-with-content",
this.snippet.getOutputDirectory()).response().content("content").build());
}
@Test
public void responseWithCustomSnippetAttributes() throws IOException {
this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents(
containsString("Title for the response"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("http-response"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/http-response-with-title.snippet"));
new HttpResponseSnippet(attributes(key("title").value("Title for the response")))
.document(new OperationBuilder("response-with-snippet-attributes",
this.snippet.getOutputDirectory()).attribute(
TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).build());
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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.hypermedia;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.StandardOperationResponse;
/**
* Tests for {@link ContentTypeLinkExtractor}.
*
* @author Andy Wilkinson
*/
public class ContentTypeLinkExtractorTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void extractionFailsWithNullContentType() throws IOException {
this.thrown.expect(IllegalStateException.class);
new ContentTypeLinkExtractor().extractLinks(new StandardOperationResponse(
HttpStatus.OK, new HttpHeaders(), null));
}
@Test
public void extractorCalledWithMatchingContextType() throws IOException {
Map<MediaType, LinkExtractor> extractors = new HashMap<>();
LinkExtractor extractor = mock(LinkExtractor.class);
extractors.put(MediaType.APPLICATION_JSON, extractor);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK,
httpHeaders, null);
new ContentTypeLinkExtractor(extractors).extractLinks(response);
verify(extractor).extractLinks(response);
}
@Test
public void extractorCalledWithCompatibleContextType() throws IOException {
Map<MediaType, LinkExtractor> extractors = new HashMap<>();
LinkExtractor extractor = mock(LinkExtractor.class);
extractors.put(MediaType.APPLICATION_JSON, extractor);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.parseMediaType("application/json;foo=bar"));
StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK,
httpHeaders, null);
new ContentTypeLinkExtractor(extractors).extractLinks(response);
verify(extractor).extractLinks(response);
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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.hypermedia;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.StandardOperationResponse;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Parameterized tests for {@link HalLinkExtractor} and {@link AtomLinkExtractor} with
* various payloads.
*
* @author Andy Wilkinson
*/
@RunWith(Parameterized.class)
public class LinkExtractorsPayloadTests {
private final LinkExtractor linkExtractor;
private final String linkType;
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[] { new HalLinkExtractor(), "hal" },
new Object[] { new AtomLinkExtractor(), "atom" });
}
public LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) {
this.linkExtractor = linkExtractor;
this.linkType = linkType;
}
@Test
public void singleLink() throws IOException {
Map<String, List<Link>> links = this.linkExtractor
.extractLinks(createResponse("single-link"));
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com")), links);
}
@Test
public void multipleLinksWithDifferentRels() throws IOException {
Map<String, List<Link>> links = this.linkExtractor
.extractLinks(createResponse("multiple-links-different-rels"));
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com"),
new Link("bravo", "http://bravo.example.com")), links);
}
@Test
public void multipleLinksWithSameRels() throws IOException {
Map<String, List<Link>> links = this.linkExtractor
.extractLinks(createResponse("multiple-links-same-rels"));
assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com/one"),
new Link("alpha", "http://alpha.example.com/two")), links);
}
@Test
public void noLinks() throws IOException {
Map<String, List<Link>> links = this.linkExtractor
.extractLinks(createResponse("no-links"));
assertLinks(Collections.<Link> emptyList(), links);
}
@Test
public void linksInTheWrongFormat() throws IOException {
Map<String, List<Link>> links = this.linkExtractor
.extractLinks(createResponse("wrong-format"));
assertLinks(Collections.<Link> emptyList(), links);
}
private void assertLinks(List<Link> expectedLinks, Map<String, List<Link>> actualLinks) {
MultiValueMap<String, Link> expectedLinksByRel = new LinkedMultiValueMap<>();
for (Link expectedLink : expectedLinks) {
expectedLinksByRel.add(expectedLink.getRel(), expectedLink);
}
assertEquals(expectedLinksByRel, actualLinks);
}
private OperationResponse createResponse(String contentName) throws IOException {
return new StandardOperationResponse(HttpStatus.OK, null,
FileCopyUtils.copyToByteArray(getPayloadFile(contentName)));
}
private File getPayloadFile(String name) {
return new File("src/test/resources/link-payloads/" + this.linkType + "/" + name
+ ".json");
}
}

View File

@@ -0,0 +1,186 @@
/*
* 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.hypermedia;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
import org.springframework.restdocs.test.OperationBuilder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Tests for {@link LinksSnippet}
*
* @author Andy Wilkinson
*/
public class LinksSnippetTests {
@Rule
public ExpectedSnippet snippet = new ExpectedSnippet();
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void undocumentedLink() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " documented: [foo]"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")),
Collections.<LinkDescriptor> emptyList()).document(new OperationBuilder(
"undocumented-link", this.snippet.getOutputDirectory()).build());
}
@Test
public void missingLink() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " found in the response: [foo]"));
new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo")
.description("bar"))).document(new OperationBuilder("missing-link",
this.snippet.getOutputDirectory()).build());
}
@Test
public void documentedOptionalLink() throws IOException {
this.snippet.expectLinks("documented-optional-link").withContents( //
tableWithHeader("Relation", "Description") //
.row("foo", "bar"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")),
Arrays.asList(new LinkDescriptor("foo").description("bar").optional()))
.document(new OperationBuilder("documented-optional-link", this.snippet
.getOutputDirectory()).build());
}
@Test
public void missingOptionalLink() throws IOException {
this.snippet.expectLinks("missing-optional-link").withContents( //
tableWithHeader("Relation", "Description") //
.row("foo", "bar"));
new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo")
.description("bar").optional())).document(new OperationBuilder(
"missing-optional-link", this.snippet.getOutputDirectory()).build());
}
@Test
public void undocumentedLinkAndMissingLink() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " documented: [a]. Links with the following relations were not"
+ " found in the response: [foo]"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")),
Arrays.asList(new LinkDescriptor("foo").description("bar")))
.document(new OperationBuilder("undocumented-link-and-missing-link",
this.snippet.getOutputDirectory()).build());
}
@Test
public void documentedLinks() throws IOException {
this.snippet.expectLinks("documented-links").withContents( //
tableWithHeader("Relation", "Description") //
.row("a", "one") //
.row("b", "two"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"),
new Link("b", "bravo")), Arrays.asList(
new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two")))
.document(new OperationBuilder("documented-links", this.snippet
.getOutputDirectory()).build());
}
@Test
public void linksWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( //
tableWithHeader("Relation", "Description", "Foo") //
.row("a", "one", "alpha") //
.row("b", "two", "bravo"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("links"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/links-with-extra-column.snippet"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"),
new Link("b", "bravo")), Arrays.asList(
new LinkDescriptor("a").description("one").attributes(
key("foo").value("alpha")),
new LinkDescriptor("b").description("two").attributes(
key("foo").value("bravo")))).document(new OperationBuilder(
"links-with-custom-descriptor-attributes", this.snippet
.getOutputDirectory()).attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).build());
}
@Test
public void linksWithCustomAttributes() throws IOException {
this.snippet.expectLinks("links-with-custom-attributes").withContents(
startsWith(".Title for the links"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("links"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/links-with-title.snippet"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"),
new Link("b", "bravo")), attributes(key("title").value(
"Title for the links")), Arrays.asList(
new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two")))
.document(new OperationBuilder("links-with-custom-attributes",
this.snippet.getOutputDirectory()).attribute(
TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver)).build());
}
private static class StubLinkExtractor implements LinkExtractor {
private MultiValueMap<String, Link> linksByRel = new LinkedMultiValueMap<String, Link>();
@Override
public MultiValueMap<String, Link> extractLinks(OperationResponse response)
throws IOException {
return this.linksByRel;
}
private StubLinkExtractor withLinks(Link... links) {
for (Link link : links) {
this.linksByRel.add(link.getRel(), link);
}
return this;
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.operation.preprocess;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.net.URI;
import java.util.Collections;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.Parameters;
import org.springframework.restdocs.operation.StandardOperationRequest;
import org.springframework.restdocs.operation.StandardOperationResponse;
/**
* Tests for {@link ContentModifyingOperationPreprocessor}
*
* @author Andy Wilkinson
*
*/
public class ContentModifyingOperationPreprocessorTests {
private final ContentModifyingOperationPreprocessor preprocessor = new ContentModifyingOperationPreprocessor(
new ContentModifier() {
@Override
public byte[] modifyContent(byte[] originalContent) {
return "modified".getBytes();
}
});
@Test
public void modifyRequestContent() {
StandardOperationRequest request = new StandardOperationRequest(
URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(),
new HttpHeaders(), new Parameters(),
Collections.<OperationRequestPart> emptyList());
OperationRequest preprocessed = this.preprocessor.preprocess(request);
assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes())));
}
@Test
public void modifyResponseContent() {
StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK,
new HttpHeaders(), "content".getBytes());
OperationResponse preprocessed = this.preprocessor.preprocess(response);
assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes())));
}
@Test
public void unknownContentLengthIsUnchanged() {
StandardOperationRequest request = new StandardOperationRequest(
URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(),
new HttpHeaders(), new Parameters(),
Collections.<OperationRequestPart> emptyList());
OperationRequest preprocessed = this.preprocessor.preprocess(request);
assertThat(preprocessed.getHeaders().getContentLength(), is(equalTo(-1L)));
}
@Test
public void contentLengthIsUpdated() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentLength(7);
StandardOperationRequest request = new StandardOperationRequest(
URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(),
httpHeaders, new Parameters(),
Collections.<OperationRequestPart> emptyList());
OperationRequest preprocessed = this.preprocessor.preprocess(request);
assertThat(preprocessed.getHeaders().getContentLength(), is(equalTo(8L)));
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.operation.preprocess;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.restdocs.operation.OperationRequest;
/**
* Tests for {@link DelegatingOperationRequestPreprocessor}
*
* @author Andy Wilkinson
*/
public class DelegatingOperationRequestPreprocessorTests {
@Test
public void delegationOccurs() {
OperationRequest originalRequest = mock(OperationRequest.class);
OperationPreprocessor preprocessor1 = mock(OperationPreprocessor.class);
OperationRequest preprocessedRequest1 = mock(OperationRequest.class);
when(preprocessor1.preprocess(originalRequest)).thenReturn(preprocessedRequest1);
OperationPreprocessor preprocessor2 = mock(OperationPreprocessor.class);
OperationRequest preprocessedRequest2 = mock(OperationRequest.class);
when(preprocessor2.preprocess(preprocessedRequest1)).thenReturn(
preprocessedRequest2);
OperationPreprocessor preprocessor3 = mock(OperationPreprocessor.class);
OperationRequest preprocessedRequest3 = mock(OperationRequest.class);
when(preprocessor3.preprocess(preprocessedRequest2)).thenReturn(
preprocessedRequest3);
OperationRequest result = new DelegatingOperationRequestPreprocessor(
Arrays.asList(preprocessor1, preprocessor2, preprocessor3))
.preprocess(originalRequest);
assertThat(result, is(preprocessedRequest3));
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.operation.preprocess;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.restdocs.operation.OperationResponse;
/**
* Tests for {@link DelegatingOperationResponsePreprocessor}
*
* @author Andy Wilkinson
*/
public class DelegatingOperationResponsePreprocessorTests {
@Test
public void delegationOccurs() {
OperationResponse originalResponse = mock(OperationResponse.class);
OperationPreprocessor preprocessor1 = mock(OperationPreprocessor.class);
OperationResponse preprocessedResponse1 = mock(OperationResponse.class);
when(preprocessor1.preprocess(originalResponse))
.thenReturn(preprocessedResponse1);
OperationPreprocessor preprocessor2 = mock(OperationPreprocessor.class);
OperationResponse preprocessedResponse2 = mock(OperationResponse.class);
when(preprocessor2.preprocess(preprocessedResponse1)).thenReturn(
preprocessedResponse2);
OperationPreprocessor preprocessor3 = mock(OperationPreprocessor.class);
OperationResponse preprocessedResponse3 = mock(OperationResponse.class);
when(preprocessor3.preprocess(preprocessedResponse2)).thenReturn(
preprocessedResponse3);
OperationResponse result = new DelegatingOperationResponsePreprocessor(
Arrays.asList(preprocessor1, preprocessor2, preprocessor3))
.preprocess(originalResponse);
assertThat(result, is(preprocessedResponse3));
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.operation.preprocess;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasEntry;
import static org.junit.Assert.assertThat;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.operation.OperationRequestPart;
import org.springframework.restdocs.operation.OperationResponse;
import org.springframework.restdocs.operation.Parameters;
import org.springframework.restdocs.operation.StandardOperationRequest;
import org.springframework.restdocs.operation.StandardOperationResponse;
/**
* Tests for {@link HeaderRemovingOperationPreprocessorTests}
*
* @author Andy Wilkinson
*
*/
public class HeaderRemovingOperationPreprocessorTests {
private final HeaderRemovingOperationPreprocessor preprocessor = new HeaderRemovingOperationPreprocessor(
"b");
@Test
public void modifyRequestHeaders() {
StandardOperationRequest request = new StandardOperationRequest(
URI.create("http://localhost"), HttpMethod.GET, new byte[0],
getHttpHeaders(), new Parameters(),
Collections.<OperationRequestPart> emptyList());
OperationRequest preprocessed = this.preprocessor.preprocess(request);
assertThat(preprocessed.getHeaders().size(), is(equalTo(1)));
assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha")));
}
@Test
public void modifyResponseHeaders() {
StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK,
getHttpHeaders(), new byte[0]);
OperationResponse preprocessed = this.preprocessor.preprocess(response);
assertThat(preprocessed.getHeaders().size(), is(equalTo(1)));
assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha")));
}
private HttpHeaders getHttpHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("a", "alpha");
httpHeaders.add("b", "bravo");
httpHeaders.add("b", "banana");
return httpHeaders;
}
}

View File

@@ -0,0 +1,156 @@
/*
* 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.operation.preprocess;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.restdocs.hypermedia.Link;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* Tests for {@link LinkMaskingContentModifier}
*
* @author Andy Wilkinson
*
*/
public class LinkMaskingContentModifierTests {
private final ContentModifier contentModifier = new LinkMaskingContentModifier();
private final Link[] links = new Link[] { new Link("a", "alpha"),
new Link("b", "bravo") };
private final Link[] maskedLinks = new Link[] { new Link("a", "..."),
new Link("b", "...") };
@Test
public void halLinksAreMasked() throws Exception {
assertThat(this.contentModifier.modifyContent(halPayloadWithLinks(this.links)),
is(equalTo(halPayloadWithLinks(this.maskedLinks))));
}
@Test
public void formattedHalLinksAreMasked() throws Exception {
assertThat(
this.contentModifier
.modifyContent(formattedHalPayloadWithLinks(this.links)),
is(equalTo(formattedHalPayloadWithLinks(this.maskedLinks))));
}
@Test
public void atomLinksAreMasked() throws Exception {
assertThat(this.contentModifier.modifyContent(atomPayloadWithLinks(this.links)),
is(equalTo(atomPayloadWithLinks(this.maskedLinks))));
}
@Test
public void formattedAtomLinksAreMasked() throws Exception {
assertThat(
this.contentModifier
.modifyContent(formattedAtomPayloadWithLinks(this.links)),
is(equalTo(formattedAtomPayloadWithLinks(this.maskedLinks))));
}
@Test
public void maskCanBeCustomized() throws Exception {
assertThat(
new LinkMaskingContentModifier("custom")
.modifyContent(formattedAtomPayloadWithLinks(this.links)),
is(equalTo(formattedAtomPayloadWithLinks(new Link("a", "custom"),
new Link("b", "custom")))));
}
private byte[] atomPayloadWithLinks(Link... links) throws JsonProcessingException {
return new ObjectMapper().writeValueAsBytes(createAtomPayload(links));
}
private byte[] formattedAtomPayloadWithLinks(Link... links)
throws JsonProcessingException {
return new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true)
.writeValueAsBytes(createAtomPayload(links));
}
private AtomPayload createAtomPayload(Link... links) {
AtomPayload payload = new AtomPayload();
payload.setLinks(Arrays.asList(links));
return payload;
}
private byte[] halPayloadWithLinks(Link... links) throws JsonProcessingException {
return new ObjectMapper().writeValueAsBytes(createHalPayload(links));
}
private byte[] formattedHalPayloadWithLinks(Link... links)
throws JsonProcessingException {
return new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true)
.writeValueAsBytes(createHalPayload(links));
}
private HalPayload createHalPayload(Link... links) {
HalPayload payload = new HalPayload();
Map<String, Object> linksMap = new LinkedHashMap<>();
for (Link link : links) {
Map<String, String> linkMap = new HashMap<>();
linkMap.put("href", link.getHref());
linksMap.put(link.getRel(), linkMap);
}
payload.setLinks(linksMap);
return payload;
}
static final class AtomPayload {
private List<Link> links;
public void setLinks(List<Link> links) {
this.links = links;
}
public List<Link> getLinks() {
return this.links;
}
}
static final class HalPayload {
private Map<String, Object> links;
@JsonProperty("_links")
public Map<String, Object> getLinks() {
return this.links;
}
public void setLinks(Map<String, Object> links) {
this.links = links;
}
}
}

Some files were not shown because too many files have changed in this diff Show More