package org.neo4j.smack.test.util; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import org.neo4j.test.AsciiDocGenerator; import org.neo4j.test.GraphDefinition; import org.neo4j.test.TestData.Producer; import org.neo4j.visualization.asciidoc.AsciidocHelper; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientRequest; import com.sun.jersey.api.client.ClientRequest.Builder; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.UniformInterfaceException; /** * Generate asciidoc-formatted documentation from HTTP requests and responses. * The status and media type of all responses is checked as well as the * existence of any expected headers. * * The filename of the resulting ASCIIDOC test file is derived from the title. * * The title is determined by either a JavaDoc perioed terminated first title line, * the @Title annotation or the method name, where "_" is replaced by " ". */ public class RESTDocsGenerator extends AsciiDocGenerator { private static final Builder REQUEST_BUILDER = ClientRequest.create(); private static final List<String> RESPONSE_HEADERS = Arrays.asList( new String[] { "Content-Type", "Location" } ); private static final List<String> REQUEST_HEADERS = Arrays.asList( new String[] { "Content-Type", "Accept" } ); public static final Producer<RESTDocsGenerator> PRODUCER = new Producer<RESTDocsGenerator>() { @Override public RESTDocsGenerator create( GraphDefinition graph, String title, String documentation ) { RESTDocsGenerator gen = RESTDocsGenerator.create( title ); gen.description(documentation); return gen; } @Override public void destroy( RESTDocsGenerator product, boolean successful ) { // TODO: invoke some complete method here? } }; private int expectedResponseStatus = -1; private MediaType expectedMediaType = MediaType.APPLICATION_JSON_TYPE; private MediaType payloadMediaType = MediaType.APPLICATION_JSON_TYPE; private final List<String> expectedHeaderFields = new ArrayList<String>(); private String payload; /** * Creates a documented test case. Finish building it by using one of these: * {@link #get(String)}, {@link #post(String)}, {@link #put(String)}, * {@link #delete(String)}, {@link #request(ClientRequest)}. To access the * response, use {@link ResponseEntity#entity} to get the entity or * {@link ResponseEntity#response} to get the rest of the response * (excluding the entity). * * @param title title of the test */ public static RESTDocsGenerator create( final String title ) { if ( title == null ) { throw new IllegalArgumentException( "The title can not be null" ); } return new RESTDocsGenerator( title ); } private RESTDocsGenerator( String ti ) { super(ti, "rest-api"); } /** * Set the expected status of the response. The test will fail if the * response has a different status. Defaults to HTTP 200 OK. * * @param expectedResponseStatus the expected response status */ public RESTDocsGenerator expectedStatus( final int expectedResponseStatus ) { this.expectedResponseStatus = expectedResponseStatus; return this; } /** * Set the expected status of the response. The test will fail if the * response has a different status. Defaults to HTTP 200 OK. * * @param expectedResponseStatus the expected response status */ public RESTDocsGenerator expectedStatus( final ClientResponse.Status expectedStatus) { this.expectedResponseStatus = expectedStatus.getStatusCode(); return this; } /** * Set the expected media type of the response. The test will fail if the * response has a different media type. Defaults to application/json. * * @param expectedMediaType the expected media tyupe */ public RESTDocsGenerator expectedType( final MediaType expectedMediaType ) { this.expectedMediaType = expectedMediaType; return this; } /** * The media type of the request payload. Defaults to application/json. * * @param payloadMediaType the media type to use */ public RESTDocsGenerator payloadType( final MediaType payloadMediaType ) { this.payloadMediaType = payloadMediaType; return this; } /** * Set the payload of the request. * * @param payload the payload */ public RESTDocsGenerator payload( final String payload ) { this.payload = payload; return this; } /** * Add an expected response header. If the heading is missing in the * response the test will fail. The header and its value are also included * in the documentation. * * @param expectedHeaderField the expected header */ public RESTDocsGenerator expectedHeader( final String expectedHeaderField ) { this.expectedHeaderFields.add( expectedHeaderField ); return this; } /** * Send a request using your own request object. * * @param request the request to perform */ public ResponseEntity request( final ClientRequest request ) { return retrieveResponse( title, description, request.getURI() .toString(), expectedResponseStatus, expectedMediaType, expectedHeaderFields, request ); } @Override public RESTDocsGenerator description( String description ) { return (RESTDocsGenerator) super.description( description ); } /** * Send a GET request. * * @param uri the URI to use. */ public ResponseEntity get( final String uri ) { return retrieveResponseFromRequest( title, description, "GET", uri, expectedResponseStatus, expectedMediaType, expectedHeaderFields ); } /** * Send a POST request. * * @param uri the URI to use. */ public ResponseEntity post( final String uri ) { return retrieveResponseFromRequest( title, description, "POST", uri, payload, payloadMediaType, expectedResponseStatus, expectedMediaType, expectedHeaderFields ); } /** * Send a PUT request. * * @param uri the URI to use. */ public ResponseEntity put( final String uri ) { return retrieveResponseFromRequest( title, description, "PUT", uri, payload, payloadMediaType, expectedResponseStatus, expectedMediaType, expectedHeaderFields ); } /** * Send a DELETE request. * * @param uri the URI to use. */ public ResponseEntity delete( final String uri ) { return retrieveResponseFromRequest( title, description, "DELETE", uri, payload, payloadMediaType, expectedResponseStatus, expectedMediaType, expectedHeaderFields ); } /** * Send a request with no payload. */ private ResponseEntity retrieveResponseFromRequest( final String title, final String description, final String method, final String uri, final int responseCode, final MediaType accept, final List<String> headerFields ) { ClientRequest request; try { request = REQUEST_BUILDER.accept( accept ) .build( new URI( uri ), method ); } catch ( URISyntaxException e ) { throw new RuntimeException( e ); } return retrieveResponse( title, description, uri, responseCode, accept, headerFields, request ); } /** * Send a request with payload. */ private ResponseEntity retrieveResponseFromRequest( final String title, final String description, final String method, final String uri, final String payload, final MediaType payloadType, final int responseCode, final MediaType accept, final List<String> headerFields ) { ClientRequest request; try { if ( payload != null ) { request = REQUEST_BUILDER.type( payloadType ) .accept( accept ) .entity( payload ) .build( new URI( uri ), method ); } else { request = REQUEST_BUILDER.accept( accept ) .build( new URI( uri ), method ); } } catch ( URISyntaxException e ) { throw new RuntimeException( e ); } return retrieveResponse( title, description, uri, responseCode, accept, headerFields, request ); } /** * Send the request and create the documentation. */ private ResponseEntity retrieveResponse( final String title, final String description, final String uri, final int responseCode, final MediaType type, final List<String> headerFields, final ClientRequest request ) { DocumentationData data = new DocumentationData(); getRequestHeaders( data, request.getHeaders() ); if ( request.getEntity() != null ) { data.setPayload( String.valueOf( request.getEntity() ) ); } Client client = new Client(); ClientResponse response = client.handle( request ); if ( response.hasEntity() && response.getStatus() != 204 ) { data.setEntity( response.getEntity( String.class ) ); } try { } catch (UniformInterfaceException uie) { //ok } if ( response.getType() != null ) { assertTrue( "wrong response type: "+ data.entity, response.getType().isCompatible( type ) ); } for ( String headerField : headerFields ) { assertNotNull( "wrong headers: "+ data.entity, response.getHeaders() .get( headerField ) ); } data.setTitle( title ); data.setDescription( description ); data.setMethod( request.getMethod() ); data.setUri( uri ); data.setStatus( responseCode ); assertEquals( "Wrong response status. response: " + data.entity, responseCode, response.getStatus() ); getResponseHeaders( data, response.getHeaders(), headerFields ); document( data ); return new ResponseEntity( response, data.entity ); } private void getResponseHeaders( final DocumentationData data, final MultivaluedMap<String, String> headers, final List<String> additionalFilter ) { data.setResponseHeaders( getHeaders( headers, RESPONSE_HEADERS, additionalFilter ) ); } private void getRequestHeaders( final DocumentationData data, final MultivaluedMap<String, Object> headers ) { data.setRequestHeaders( getHeaders( headers, REQUEST_HEADERS, Collections.<String>emptyList() ) ); } private <T> Map<String, String> getHeaders( final MultivaluedMap<String, T> headers, final List<String> filter, final List<String> additionalFilter ) { Map<String, String> filteredHeaders = new TreeMap<String, String>(); for ( Entry<String, List<T>> header : headers.entrySet() ) { if ( filter.contains( header.getKey() ) || additionalFilter.contains( header.getKey() ) ) { String values = ""; for ( T value : header.getValue() ) { if ( !values.isEmpty() ) { values += ", "; } values += String.valueOf( value ); } filteredHeaders.put( header.getKey(), values ); } } return filteredHeaders; } /** * Wraps a response, to give access to the response entity as well. */ public static class ResponseEntity { private final String entity; private final JaxRsResponse response; public ResponseEntity( ClientResponse response, String entity ) { this.response = new JaxRsResponse(response,entity); this.entity = entity; } /** * The response entity as a String. */ public String entity() { return entity; } /** * Note that the response object returned does not give access to the * response entity. */ public JaxRsResponse response() { return response; } } private class DocumentationData { public String payload; public String title; public String description; public String uri; public String method; public int status; public String entity; public Map<String, String> requestHeaders; public Map<String, String> responseHeaders; public void setPayload( final String payload ) { this.payload = payload; } public void setDescription( final String description ) { this.description = description; } public void setTitle( final String title ) { this.title = title; } public void setUri( final String uri ) { this.uri = uri; } public void setMethod( final String method ) { this.method = method; } public void setStatus( final int responseCode ) { this.status = responseCode; } public void setEntity( final String entity ) { this.entity = entity; } public void setResponseHeaders( final Map<String, String> response ) { responseHeaders = response; } public void setRequestHeaders( final Map<String, String> request ) { requestHeaders = request; } @Override public String toString() { return "DocumentationData [payload=" + payload + ", title=" + title + ", description=" + description + ", uri=" + uri + ", method=" + method + ", status=" + status + ", entity=" + entity + ", requestHeaders=" + requestHeaders + ", responseHeaders=" + responseHeaders + "]"; } } protected void document( final DocumentationData data ) { data.description = replaceSnippets( data.description ); Writer fw = null; try { fw = getFW("target" + File.separator + "docs"+ File.separator + section , data.title); String name = title.replace( " ", "-" ) .toLowerCase(); line( fw, "[["+section.replaceAll( "\\(|\\)", "" )+"-" + name.replaceAll( "\\(|\\)", "" ) + "]]" ); //make first Character uppercase String firstChar = data.title.substring( 0, 1 ).toUpperCase(); line( fw, "=== " + firstChar + data.title.substring( 1 ) + " ===" ); line( fw, "" ); if ( data.description != null && !data.description.isEmpty() ) { line( fw, data.description ); line( fw, "" ); } if( graph != null) { fw.append( AsciidocHelper.createGraphViz( "Final Graph", graph, title)); line(fw, "" ); } line( fw, "_Example request_" ); line( fw, "" ); line( fw, "* *+" + data.method + "+* +" + data.uri + "+" ); if ( data.requestHeaders != null ) { for ( Entry<String, String> header : data.requestHeaders.entrySet() ) { line( fw, "* *+" + header.getKey() + ":+* +" + header.getValue() + "+" ); } } writeEntity( fw, data.payload ); line( fw, "" ); line( fw, "_Example response_" ); line( fw, "" ); line( fw, "* *+" + data.status + ":+* +" + Response.Status.fromStatusCode( data.status ) + "+" ); if ( data.responseHeaders != null ) { for ( Entry<String, String> header : data.responseHeaders.entrySet() ) { line( fw, "* *+" + header.getKey() + ":+* +" + header.getValue() + "+" ); } } writeEntity( fw, data.entity ); line( fw, "" ); } catch ( IOException e ) { e.printStackTrace(); fail(); } finally { if ( fw != null ) { try { fw.close(); } catch ( IOException e ) { e.printStackTrace(); fail(); } } } } public void writeEntity( final Writer fw, final String entity ) throws IOException { if ( entity != null ) { line( fw, "[source,javascript]" ); line( fw, "----" ); line( fw, entity ); line( fw, "----" ); line( fw, "" ); } } }