Remove the need for manual documentCurlRequest/Response calls
Previously, whenever a MockMvc perform call was made and the request and response were to be documented, calls to documentCurlRequest and documentCurlResponse had to be made. This commit updates the REST documentation framework to make the documentation of the request and response automatic. It makes use of MockMvc’s MockMvcConfigurer that was introduced in Spring 4.1. Applying RestDocumentationConfiguration once will cause all requests and responses to be documented: this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(new RestDocumentationConfiguration()).build(); The path to which the requests and response documentation snippets will be written is determined by the class and method in which the MockMvc perform call is made and is currently of the form shortClassName/methodName(Request|Response|RequestResponse).asciidoc where shortClassName is the name of the documentation class without its package. This works for both @Test methods and any non-private methods that are called from the test method. The methods must be non-private as a CGLib proxy is used to intercept the calls and configure the output path.
This commit is contained in:
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014 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.core;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.PrintStream;
|
||||
|
||||
import org.springframework.restdocs.core.DocumentationWriter.DocumentationAction;
|
||||
|
||||
public abstract class Documentation {
|
||||
|
||||
public static void document(String path, DocumentationAction action) throws Exception {
|
||||
PrintStream printStream = createPrintStream(path);
|
||||
try {
|
||||
DocumentationContext.set(new DocumentationContext(printStream));
|
||||
action.perform();
|
||||
}
|
||||
finally {
|
||||
DocumentationContext.set(null);
|
||||
printStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static PrintStream createPrintStream(String name)
|
||||
throws FileNotFoundException {
|
||||
File outputFile = new File(name);
|
||||
if (!outputFile.isAbsolute()) {
|
||||
outputFile = makeAbsolute(outputFile);
|
||||
}
|
||||
outputFile.getParentFile().mkdirs();
|
||||
|
||||
return new PrintStream(new FileOutputStream(outputFile));
|
||||
}
|
||||
|
||||
private static File makeAbsolute(File outputFile) {
|
||||
return new File(new DocumentationProperties().getOutputDir(),
|
||||
outputFile.getPath());
|
||||
}
|
||||
}
|
||||
@@ -16,27 +16,47 @@
|
||||
|
||||
package org.springframework.restdocs.core;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Stack;
|
||||
|
||||
class DocumentationContext {
|
||||
|
||||
private static final InheritableThreadLocal<DocumentationContext> CONTEXTS = new InheritableThreadLocal<DocumentationContext>();
|
||||
private static final InheritableThreadLocal<Stack<DocumentationContext>> CONTEXTS = new InheritableThreadLocal<Stack<DocumentationContext>>() {
|
||||
|
||||
private final DocumentationWriter writer;
|
||||
@Override
|
||||
protected Stack<DocumentationContext> initialValue() {
|
||||
return new Stack<DocumentationContext>();
|
||||
}
|
||||
|
||||
public DocumentationContext(PrintStream printStream) {
|
||||
this.writer = new DocumentationWriter(printStream);
|
||||
}
|
||||
};
|
||||
|
||||
public DocumentationWriter getWriter() {
|
||||
return this.writer;
|
||||
private final Class<?> documentationClass;
|
||||
|
||||
private final Method documentationMethod;
|
||||
|
||||
public DocumentationContext(Class<?> documentationClass, Method documentationMethod) {
|
||||
this.documentationClass = documentationClass;
|
||||
this.documentationMethod = documentationMethod;
|
||||
}
|
||||
|
||||
public static DocumentationContext current() {
|
||||
return CONTEXTS.get();
|
||||
return CONTEXTS.get().peek();
|
||||
}
|
||||
|
||||
static void set(DocumentationContext context) {
|
||||
CONTEXTS.set(context);
|
||||
public Class<?> getDocumentationClass() {
|
||||
return documentationClass;
|
||||
}
|
||||
|
||||
public Method getDocumentationMethod() {
|
||||
return documentationMethod;
|
||||
}
|
||||
|
||||
static void push(DocumentationContext context) {
|
||||
CONTEXTS.get().push(context);
|
||||
}
|
||||
|
||||
static void pop() {
|
||||
CONTEXTS.get().pop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2014 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.core;
|
||||
|
||||
import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequest;
|
||||
import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequestAndResponse;
|
||||
import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlResponse;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
public class RestDocumentationConfiguration implements MockMvcConfigurer {
|
||||
|
||||
private String scheme = "http";
|
||||
|
||||
private String host = "localhost";
|
||||
|
||||
private int port = 8080;
|
||||
|
||||
public RestDocumentationConfiguration withScheme(String scheme) {
|
||||
this.scheme = scheme;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RestDocumentationConfiguration withHost(String host) {
|
||||
this.host = host;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RestDocumentationConfiguration withPort(int port) {
|
||||
this.port = port;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConfigurerAdded(ConfigurableMockMvcBuilder<?> builder) {
|
||||
builder.alwaysDo(documentCurlRequest().includeResponseHeaders())
|
||||
.alwaysDo(documentCurlResponse().includeResponseHeaders())
|
||||
.alwaysDo(documentCurlRequestAndResponse().includeResponseHeaders());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestPostProcessor beforeMockMvcCreated(
|
||||
ConfigurableMockMvcBuilder<?> builder, WebApplicationContext context) {
|
||||
return new RequestPostProcessor() {
|
||||
|
||||
@Override
|
||||
public MockHttpServletRequest postProcessRequest(
|
||||
MockHttpServletRequest request) {
|
||||
request.setScheme(scheme);
|
||||
request.setRemotePort(port);
|
||||
request.setServerPort(port);
|
||||
request.setRemoteHost(host);
|
||||
return request;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2014 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.core;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.runners.model.InitializationError;
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
import org.springframework.cglib.proxy.MethodInterceptor;
|
||||
import org.springframework.cglib.proxy.MethodProxy;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
public class RestDocumentationJUnit4ClassRunner extends SpringJUnit4ClassRunner {
|
||||
|
||||
public RestDocumentationJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
|
||||
super(clazz);
|
||||
}
|
||||
|
||||
protected Object createTest() throws Exception {
|
||||
Object testInstance = createProxiedTestInstance();
|
||||
getTestContextManager().prepareTestInstance(testInstance);
|
||||
return testInstance;
|
||||
}
|
||||
|
||||
private Object createProxiedTestInstance() {
|
||||
Enhancer enhancer = new Enhancer();
|
||||
enhancer.setSuperclass(getTestClass().getJavaClass());
|
||||
enhancer.setClassLoader(getTestClass().getJavaClass().getClassLoader());
|
||||
enhancer.setCallback(new DocumentationContextManagingMethodInterceptor(
|
||||
getTestClass().getJavaClass()));
|
||||
return enhancer.create();
|
||||
}
|
||||
|
||||
private static class DocumentationContextManagingMethodInterceptor implements
|
||||
MethodInterceptor {
|
||||
|
||||
private final Class<?> testClass;
|
||||
|
||||
private DocumentationContextManagingMethodInterceptor(Class<?> testClass) {
|
||||
this.testClass = testClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object intercept(Object target, Method method, Object[] args,
|
||||
MethodProxy methodProxy) throws Throwable {
|
||||
DocumentationContext.push(new DocumentationContext(this.testClass, method));
|
||||
try {
|
||||
return methodProxy.invokeSuper(target, args);
|
||||
}
|
||||
finally {
|
||||
DocumentationContext.pop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014 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.core;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
|
||||
public abstract class RestDocumentationRequestPostProcessors {
|
||||
|
||||
public static RequestPostProcessor port(final int port) {
|
||||
return new RequestPostProcessor() {
|
||||
|
||||
@Override
|
||||
public MockHttpServletRequest postProcessRequest(
|
||||
MockHttpServletRequest request) {
|
||||
request.setRemotePort(port);
|
||||
request.setServerPort(port);
|
||||
return request;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public static RequestPostProcessor host(final String host) {
|
||||
return new RequestPostProcessor() {
|
||||
|
||||
@Override
|
||||
public MockHttpServletRequest postProcessRequest(
|
||||
MockHttpServletRequest request) {
|
||||
request.setRemoteHost(host);
|
||||
return request;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.restdocs.core;
|
||||
|
||||
import static org.springframework.restdocs.core.IterableEnumeration.iterable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -31,12 +33,10 @@ import org.springframework.test.web.servlet.ResultHandler;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import static org.springframework.restdocs.core.IterableEnumeration.iterable;
|
||||
|
||||
public abstract class RestDocumentationResultHandlers {
|
||||
|
||||
public static CurlResultHandler documentCurlRequest(String path) {
|
||||
return new CurlResultHandler(path) {
|
||||
public static CurlResultHandler documentCurlRequest() {
|
||||
return new CurlResultHandler("Request.asciidoc") {
|
||||
@Override
|
||||
public void handle(MvcResult result, DocumentationWriter writer)
|
||||
throws Exception {
|
||||
@@ -46,8 +46,8 @@ public abstract class RestDocumentationResultHandlers {
|
||||
};
|
||||
}
|
||||
|
||||
public static CurlResultHandler documentCurlResponse(String path) {
|
||||
return new CurlResultHandler(path) {
|
||||
public static CurlResultHandler documentCurlResponse() {
|
||||
return new CurlResultHandler("Response.asciidoc") {
|
||||
@Override
|
||||
public void handle(MvcResult result, DocumentationWriter writer)
|
||||
throws Exception {
|
||||
@@ -57,8 +57,8 @@ public abstract class RestDocumentationResultHandlers {
|
||||
};
|
||||
}
|
||||
|
||||
public static CurlResultHandler documentCurlRequestAndResponse(String path) {
|
||||
return new CurlResultHandler(path) {
|
||||
public static CurlResultHandler documentCurlRequestAndResponse() {
|
||||
return new CurlResultHandler("RequestResponse.asciidoc") {
|
||||
@Override
|
||||
public void handle(MvcResult result, DocumentationWriter writer)
|
||||
throws Exception {
|
||||
@@ -70,22 +70,6 @@ public abstract class RestDocumentationResultHandlers {
|
||||
};
|
||||
}
|
||||
|
||||
private static PrintStream createPrintStream(String path)
|
||||
throws FileNotFoundException {
|
||||
File outputFile = new File(path);
|
||||
if (!outputFile.isAbsolute()) {
|
||||
outputFile = makeAbsolute(outputFile);
|
||||
}
|
||||
outputFile.getParentFile().mkdirs();
|
||||
|
||||
return new PrintStream(new FileOutputStream(outputFile));
|
||||
}
|
||||
|
||||
private static File makeAbsolute(File outputFile) {
|
||||
return new File(new DocumentationProperties().getOutputDir(),
|
||||
outputFile.getPath());
|
||||
}
|
||||
|
||||
private static final class CurlRequestDocumentationAction implements
|
||||
DocumentationAction {
|
||||
|
||||
@@ -182,11 +166,11 @@ public abstract class RestDocumentationResultHandlers {
|
||||
public static abstract class CurlResultHandler implements ResultHandler {
|
||||
|
||||
private final CurlConfiguration curlConfiguration = new CurlConfiguration();
|
||||
|
||||
private final String path;
|
||||
|
||||
private CurlResultHandler(String path) {
|
||||
this.path = path;
|
||||
|
||||
private String suffix;
|
||||
|
||||
public CurlResultHandler(String suffix) {
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
CurlConfiguration getCurlConfiguration() {
|
||||
@@ -200,7 +184,7 @@ public abstract class RestDocumentationResultHandlers {
|
||||
|
||||
@Override
|
||||
public void handle(MvcResult result) throws Exception {
|
||||
PrintStream printStream = createPrintStream(this.path);
|
||||
PrintStream printStream = createPrintStream(this.suffix);
|
||||
try {
|
||||
handle(result, new DocumentationWriter(printStream));
|
||||
}
|
||||
@@ -208,6 +192,42 @@ public abstract class RestDocumentationResultHandlers {
|
||||
printStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
private PrintStream createPrintStream(String suffix)
|
||||
throws FileNotFoundException {
|
||||
DocumentationContext context = DocumentationContext.current();
|
||||
if (context == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
String path = resolveOutputPath(context);
|
||||
|
||||
File outputFile = new File(path);
|
||||
if (!outputFile.isAbsolute()) {
|
||||
outputFile = makeAbsolute(outputFile);
|
||||
}
|
||||
outputFile.getParentFile().mkdirs();
|
||||
|
||||
return new PrintStream(new FileOutputStream(outputFile));
|
||||
}
|
||||
|
||||
private static File makeAbsolute(File outputFile) {
|
||||
return new File(new DocumentationProperties().getOutputDir(),
|
||||
outputFile.getPath());
|
||||
}
|
||||
|
||||
private String resolveOutputPath(DocumentationContext context) {
|
||||
String shortClassName = getShortClassName(context.getDocumentationClass());
|
||||
return shortClassName + "/" + context.getDocumentationMethod().getName() + this.suffix;
|
||||
}
|
||||
|
||||
private String getShortClassName(Class<?> clazz) {
|
||||
int index = clazz.getName().lastIndexOf('.');
|
||||
if (index >= 0) {
|
||||
return clazz.getName().substring(index + 1);
|
||||
}
|
||||
return clazz.getName();
|
||||
}
|
||||
|
||||
abstract void handle(MvcResult result, DocumentationWriter writer)
|
||||
throws Exception;
|
||||
|
||||
Reference in New Issue
Block a user