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:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 + "'");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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/{name}.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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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())));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
[source,bash]
|
||||
----
|
||||
$ curl {{arguments}}
|
||||
----
|
||||
@@ -0,0 +1,8 @@
|
||||
[source,http]
|
||||
----
|
||||
{{method}} {{path}} HTTP/1.1
|
||||
{{#headers}}
|
||||
{{name}}: {{value}}
|
||||
{{/headers}}
|
||||
{{requestBody}}
|
||||
----
|
||||
@@ -0,0 +1,8 @@
|
||||
[source,http]
|
||||
----
|
||||
HTTP/1.1 {{statusCode}} {{statusReason}}
|
||||
{{#headers}}
|
||||
{{name}}: {{value}}
|
||||
{{/headers}}
|
||||
{{responseBody}}
|
||||
----
|
||||
@@ -0,0 +1,9 @@
|
||||
|===
|
||||
|Relation|Description
|
||||
|
||||
{{#links}}
|
||||
|{{rel}}
|
||||
|{{description}}
|
||||
|
||||
{{/links}}
|
||||
|===
|
||||
@@ -0,0 +1,10 @@
|
||||
.{{path}}
|
||||
|===
|
||||
|Parameter|Description
|
||||
|
||||
{{#parameters}}
|
||||
|{{name}}
|
||||
|{{description}}
|
||||
|
||||
{{/parameters}}
|
||||
|===
|
||||
@@ -0,0 +1,10 @@
|
||||
|===
|
||||
|Path|Type|Description
|
||||
|
||||
{{#fields}}
|
||||
|{{path}}
|
||||
|{{type}}
|
||||
|{{description}}
|
||||
|
||||
{{/fields}}
|
||||
|===
|
||||
@@ -0,0 +1,9 @@
|
||||
|===
|
||||
|Parameter|Description
|
||||
|
||||
{{#parameters}}
|
||||
|{{name}}
|
||||
|{{description}}
|
||||
|
||||
{{/parameters}}
|
||||
|===
|
||||
@@ -0,0 +1,10 @@
|
||||
|===
|
||||
|Path|Type|Description
|
||||
|
||||
{{#fields}}
|
||||
|{{path}}
|
||||
|{{type}}
|
||||
|{{description}}
|
||||
|
||||
{{/fields}}
|
||||
|===
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user