@@ -0,0 +1,9 @@ | |||
*.iml | |||
.idea | |||
target | |||
tmp | |||
logs | |||
dependency-reduced-pom.xml | |||
velocity.log | |||
*~ | |||
build.log |
@@ -0,0 +1,202 @@ | |||
Apache License | |||
Version 2.0, January 2004 | |||
http://www.apache.org/licenses/ | |||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||
1. Definitions. | |||
"License" shall mean the terms and conditions for use, reproduction, | |||
and distribution as defined by Sections 1 through 9 of this document. | |||
"Licensor" shall mean the copyright owner or entity authorized by | |||
the copyright owner that is granting the License. | |||
"Legal Entity" shall mean the union of the acting entity and all | |||
other entities that control, are controlled by, or are under common | |||
control with that entity. For the purposes of this definition, | |||
"control" means (i) the power, direct or indirect, to cause the | |||
direction or management of such entity, whether by contract or | |||
otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||
outstanding shares, or (iii) beneficial ownership of such entity. | |||
"You" (or "Your") shall mean an individual or Legal Entity | |||
exercising permissions granted by this License. | |||
"Source" form shall mean the preferred form for making modifications, | |||
including but not limited to software source code, documentation | |||
source, and configuration files. | |||
"Object" form shall mean any form resulting from mechanical | |||
transformation or translation of a Source form, including but | |||
not limited to compiled object code, generated documentation, | |||
and conversions to other media types. | |||
"Work" shall mean the work of authorship, whether in Source or | |||
Object form, made available under the License, as indicated by a | |||
copyright notice that is included in or attached to the work | |||
(an example is provided in the Appendix below). | |||
"Derivative Works" shall mean any work, whether in Source or Object | |||
form, that is based on (or derived from) the Work and for which the | |||
editorial revisions, annotations, elaborations, or other modifications | |||
represent, as a whole, an original work of authorship. For the purposes | |||
of this License, Derivative Works shall not include works that remain | |||
separable from, or merely link (or bind by name) to the interfaces of, | |||
the Work and Derivative Works thereof. | |||
"Contribution" shall mean any work of authorship, including | |||
the original version of the Work and any modifications or additions | |||
to that Work or Derivative Works thereof, that is intentionally | |||
submitted to Licensor for inclusion in the Work by the copyright owner | |||
or by an individual or Legal Entity authorized to submit on behalf of | |||
the copyright owner. For the purposes of this definition, "submitted" | |||
means any form of electronic, verbal, or written communication sent | |||
to the Licensor or its representatives, including but not limited to | |||
communication on electronic mailing lists, source code control systems, | |||
and issue tracking systems that are managed by, or on behalf of, the | |||
Licensor for the purpose of discussing and improving the Work, but | |||
excluding communication that is conspicuously marked or otherwise | |||
designated in writing by the copyright owner as "Not a Contribution." | |||
"Contributor" shall mean Licensor and any individual or Legal Entity | |||
on behalf of whom a Contribution has been received by Licensor and | |||
subsequently incorporated within the Work. | |||
2. Grant of Copyright License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
copyright license to reproduce, prepare Derivative Works of, | |||
publicly display, publicly perform, sublicense, and distribute the | |||
Work and such Derivative Works in Source or Object form. | |||
3. Grant of Patent License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
(except as stated in this section) patent license to make, have made, | |||
use, offer to sell, sell, import, and otherwise transfer the Work, | |||
where such license applies only to those patent claims licensable | |||
by such Contributor that are necessarily infringed by their | |||
Contribution(s) alone or by combination of their Contribution(s) | |||
with the Work to which such Contribution(s) was submitted. If You | |||
institute patent litigation against any entity (including a | |||
cross-claim or counterclaim in a lawsuit) alleging that the Work | |||
or a Contribution incorporated within the Work constitutes direct | |||
or contributory patent infringement, then any patent licenses | |||
granted to You under this License for that Work shall terminate | |||
as of the date such litigation is filed. | |||
4. Redistribution. You may reproduce and distribute copies of the | |||
Work or Derivative Works thereof in any medium, with or without | |||
modifications, and in Source or Object form, provided that You | |||
meet the following conditions: | |||
(a) You must give any other recipients of the Work or | |||
Derivative Works a copy of this License; and | |||
(b) You must cause any modified files to carry prominent notices | |||
stating that You changed the files; and | |||
(c) You must retain, in the Source form of any Derivative Works | |||
that You distribute, all copyright, patent, trademark, and | |||
attribution notices from the Source form of the Work, | |||
excluding those notices that do not pertain to any part of | |||
the Derivative Works; and | |||
(d) If the Work includes a "NOTICE" text file as part of its | |||
distribution, then any Derivative Works that You distribute must | |||
include a readable copy of the attribution notices contained | |||
within such NOTICE file, excluding those notices that do not | |||
pertain to any part of the Derivative Works, in at least one | |||
of the following places: within a NOTICE text file distributed | |||
as part of the Derivative Works; within the Source form or | |||
documentation, if provided along with the Derivative Works; or, | |||
within a display generated by the Derivative Works, if and | |||
wherever such third-party notices normally appear. The contents | |||
of the NOTICE file are for informational purposes only and | |||
do not modify the License. You may add Your own attribution | |||
notices within Derivative Works that You distribute, alongside | |||
or as an addendum to the NOTICE text from the Work, provided | |||
that such additional attribution notices cannot be construed | |||
as modifying the License. | |||
You may add Your own copyright statement to Your modifications and | |||
may provide additional or different license terms and conditions | |||
for use, reproduction, or distribution of Your modifications, or | |||
for any such Derivative Works as a whole, provided Your use, | |||
reproduction, and distribution of the Work otherwise complies with | |||
the conditions stated in this License. | |||
5. Submission of Contributions. Unless You explicitly state otherwise, | |||
any Contribution intentionally submitted for inclusion in the Work | |||
by You to the Licensor shall be under the terms and conditions of | |||
this License, without any additional terms or conditions. | |||
Notwithstanding the above, nothing herein shall supersede or modify | |||
the terms of any separate license agreement you may have executed | |||
with Licensor regarding such Contributions. | |||
6. Trademarks. This License does not grant permission to use the trade | |||
names, trademarks, service marks, or product names of the Licensor, | |||
except as required for reasonable and customary use in describing the | |||
origin of the Work and reproducing the content of the NOTICE file. | |||
7. Disclaimer of Warranty. Unless required by applicable law or | |||
agreed to in writing, Licensor provides the Work (and each | |||
Contributor provides its Contributions) on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
implied, including, without limitation, any warranties or conditions | |||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||
PARTICULAR PURPOSE. You are solely responsible for determining the | |||
appropriateness of using or redistributing the Work and assume any | |||
risks associated with Your exercise of permissions under this License. | |||
8. Limitation of Liability. In no event and under no legal theory, | |||
whether in tort (including negligence), contract, or otherwise, | |||
unless required by applicable law (such as deliberate and grossly | |||
negligent acts) or agreed to in writing, shall any Contributor be | |||
liable to You for damages, including any direct, indirect, special, | |||
incidental, or consequential damages of any character arising as a | |||
result of this License or out of the use or inability to use the | |||
Work (including but not limited to damages for loss of goodwill, | |||
work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses), even if such Contributor | |||
has been advised of the possibility of such damages. | |||
9. Accepting Warranty or Additional Liability. While redistributing | |||
the Work or Derivative Works thereof, You may choose to offer, | |||
and charge a fee for, acceptance of support, warranty, indemnity, | |||
or other liability obligations and/or rights consistent with this | |||
License. However, in accepting such obligations, You may act only | |||
on Your own behalf and on Your sole responsibility, not on behalf | |||
of any other Contributor, and only if You agree to indemnify, | |||
defend, and hold each Contributor harmless for any liability | |||
incurred by, or claims asserted against, such Contributor by reason | |||
of your accepting any such warranty or additional liability. | |||
END OF TERMS AND CONDITIONS | |||
APPENDIX: How to apply the Apache License to your work. | |||
To apply the Apache License to your work, attach the following | |||
boilerplate notice, with the fields enclosed by brackets "[]" | |||
replaced with your own identifying information. (Don't include | |||
the brackets!) The text should be enclosed in the appropriate | |||
comment syntax for the file format. We also recommend that a | |||
file or class name and description of purpose be included on the | |||
same "printed page" as the copyright notice for easier | |||
identification within third-party archives. | |||
restex is Copyright 2013 Jonathan Cobb | |||
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. |
@@ -0,0 +1,41 @@ | |||
restex | |||
====== | |||
Capture your REST integration tests and present them as examples for API consumers. | |||
Use this in your JUnit-style test cases to capture request/response conversations with context. | |||
protected static TemplateCaptureTarget apiDocs = new TemplateCaptureTarget("target/api-examples"); | |||
@Override | |||
protected HttpClient getHttpClient() { | |||
return new DefaultHttpClient(new RestexClientConnectionManager(apiDocs)); | |||
} | |||
@After | |||
public void commitDocCapture () throws Exception { | |||
apiDocs.commit(); | |||
} | |||
@AfterClass | |||
public static void finalizeDocCapture () throws Exception { | |||
apiDocs.close(); | |||
} | |||
@Test | |||
public void someTest() throws Exception { | |||
apiDocs.startRecording("some class of operations", "this particular operation"); | |||
apiDocs.addNote("going to do step #1"); | |||
... do step 1, something that will use the HttpClient from getHttpClient() ... | |||
apiDocs.addNote("going to do step #2"); | |||
... do step 2, something that will use the HttpClient from getHttpClient() ... | |||
} | |||
@Test | |||
public void anotherTest() throws Exception { | |||
apiDocs.startRecording("some class of operations", "another particular operation"); | |||
apiDocs.addNote("going to do step #1"); | |||
... do step 1, something that will use the HttpClient from getHttpClient() ... | |||
apiDocs.addNote("going to do step #2"); | |||
... do step 2, something that will use the HttpClient from getHttpClient() ... | |||
} |
@@ -0,0 +1,119 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<!-- | |||
(c) Copyright 2013 Jonathan Cobb | |||
This code is available under the Apache License, version 2: http://www.apache.org/licenses/LICENSE-2.0.html | |||
--> | |||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> | |||
<modelVersion>4.0.0</modelVersion> | |||
<parent> | |||
<groupId>org.cobbzilla</groupId> | |||
<artifactId>cobbzilla-parent</artifactId> | |||
<version>1.0.0-SNAPSHOT</version> | |||
</parent> | |||
<artifactId>restex</artifactId> | |||
<name>restex</name> | |||
<version>1.0.1-SNAPSHOT</version> | |||
<packaging>jar</packaging> | |||
<licenses> | |||
<license> | |||
<name>The Apache Software License, Version 2.0</name> | |||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> | |||
<distribution>repo</distribution> | |||
</license> | |||
</licenses> | |||
<scm> | |||
<url>http://github.com/cobbzilla/restex</url> | |||
<connection>scm:git:git@github.com:cobbzilla/restex.git</connection> | |||
<developerConnection>scm:git:git@github.com:cobbzilla/restex.git</developerConnection> | |||
</scm> | |||
<dependencies> | |||
<!-- httpclient --> | |||
<dependency> | |||
<groupId>org.apache.httpcomponents</groupId> | |||
<artifactId>httpcore</artifactId> | |||
<version>${httpcore.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.apache.httpcomponents</groupId> | |||
<artifactId>httpclient</artifactId> | |||
<version>${httpclient.version}</version> | |||
</dependency> | |||
<!-- Logging --> | |||
<dependency> | |||
<groupId>org.slf4j</groupId> | |||
<artifactId>slf4j-api</artifactId> | |||
<version>${slf4j.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.slf4j</groupId> | |||
<artifactId>jcl-over-slf4j</artifactId> | |||
<version>${slf4j.version}</version> | |||
<scope>runtime</scope> | |||
</dependency> | |||
<!-- Testing --> | |||
<dependency> | |||
<groupId>org.eclipse.jetty</groupId> | |||
<artifactId>jetty-server</artifactId> | |||
<version>${jetty.version}</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.eclipse.jetty</groupId> | |||
<artifactId>jetty-http</artifactId> | |||
<version>${jetty.version}</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<!-- auto-generate java boilerplate --> | |||
<dependency> | |||
<groupId>org.projectlombok</groupId> | |||
<artifactId>lombok</artifactId> | |||
<version>${lombok.version}</version> | |||
<scope>compile</scope> | |||
</dependency> | |||
<!-- handlebars templates --> | |||
<dependency> | |||
<groupId>com.github.jknack</groupId> | |||
<artifactId>handlebars</artifactId> | |||
<version>${handlebars.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.github.jknack</groupId> | |||
<artifactId>handlebars-jackson2</artifactId> | |||
<version>${handlebars.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>commons-io</groupId> | |||
<artifactId>commons-io</artifactId> | |||
<version>${commons-io.version}</version> | |||
</dependency> | |||
</dependencies> | |||
<build> | |||
<plugins> | |||
<!-- use Java 11 --> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-compiler-plugin</artifactId> | |||
<version>2.3.2</version> | |||
<configuration> | |||
<source>11</source> | |||
<target>11</target> | |||
<showWarnings>true</showWarnings> | |||
</configuration> | |||
</plugin> | |||
</plugins> | |||
</build> | |||
</project> |
@@ -0,0 +1,23 @@ | |||
package org.cobbzilla.restex; | |||
import java.io.IOException; | |||
public interface RestexCaptureTarget { | |||
public void requestUri (String method, String uri); | |||
public void requestHeader(String name, String value); | |||
public void requestEntity(String entityData); | |||
public void responseStatus(int statusCode, String reasonPhrase, String protocolVersion); | |||
public void responseHeader(String name, String value); | |||
public void responseEntity(String entityData); | |||
public void commit() throws IOException; | |||
public void setBinaryRequest(String hint); | |||
public void setBinaryResponse(String hint); | |||
} |
@@ -0,0 +1,108 @@ | |||
package org.cobbzilla.restex; | |||
import lombok.Cleanup; | |||
import lombok.Delegate; | |||
import org.apache.commons.io.IOUtils; | |||
import org.apache.http.*; | |||
import org.apache.http.conn.ManagedHttpClientConnection; | |||
import org.apache.http.entity.BasicHttpEntity; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.IOException; | |||
import java.io.InputStreamReader; | |||
import java.io.StringWriter; | |||
public class RestexClientConnection implements ManagedHttpClientConnection { | |||
private interface ConnectionIO { | |||
void sendRequestHeader(HttpRequest httpRequest) throws HttpException, IOException; | |||
void sendRequestEntity(HttpEntityEnclosingRequest httpEntityEnclosingRequest) throws HttpException, IOException; | |||
HttpResponse receiveResponseHeader() throws HttpException, IOException; | |||
void receiveResponseEntity(HttpResponse httpResponse) throws HttpException, IOException; | |||
} | |||
@Delegate(excludes=ConnectionIO.class, types={ManagedHttpClientConnection.class, HttpConnection.class}) | |||
private final ManagedHttpClientConnection delegate; | |||
private final RestexCaptureTarget target; | |||
public RestexClientConnection(ManagedHttpClientConnection connection, RestexCaptureTarget target) { | |||
this.delegate = connection; | |||
this.target = target; | |||
} | |||
@Override | |||
public void sendRequestHeader(HttpRequest httpRequest) throws HttpException, IOException { | |||
target.requestUri(httpRequest.getRequestLine().getMethod(), httpRequest.getRequestLine().getUri()); | |||
final Header[] headers = httpRequest.getAllHeaders(); | |||
for (Header header : headers) { | |||
target.requestHeader(header.getName(), header.getValue()); | |||
} | |||
delegate.sendRequestHeader(httpRequest); | |||
} | |||
@Override | |||
public void sendRequestEntity(HttpEntityEnclosingRequest httpEntityEnclosingRequest) throws HttpException, IOException { | |||
// Read the entire request into a String | |||
StringWriter writer = new StringWriter(); | |||
final HttpEntity entity = httpEntityEnclosingRequest.getEntity(); | |||
final String entityData; | |||
if (entity != null && entity.getContent() != null) { | |||
@Cleanup final InputStreamReader in = new InputStreamReader(entity.getContent()); | |||
IOUtils.copyLarge(in, writer); | |||
entityData = writer.toString(); | |||
} else { | |||
entityData = ""; | |||
} | |||
target.requestEntity(entityData); | |||
// Re-create the entity since we already read the entire InputStream | |||
httpEntityEnclosingRequest.setEntity(buildEntity(entityData)); | |||
delegate.sendRequestEntity(httpEntityEnclosingRequest); | |||
} | |||
@Override | |||
public HttpResponse receiveResponseHeader() throws HttpException, IOException { | |||
final HttpResponse httpResponse = delegate.receiveResponseHeader(); | |||
target.responseStatus(httpResponse.getStatusLine().getStatusCode(), httpResponse.getStatusLine().getReasonPhrase(), httpResponse.getStatusLine().getProtocolVersion().toString()); | |||
final Header[] headers = httpResponse.getAllHeaders(); | |||
for (Header header : headers) { | |||
target.responseHeader(header.getName(), header.getValue()); | |||
} | |||
return httpResponse; | |||
} | |||
@Override | |||
public void receiveResponseEntity(HttpResponse response) throws HttpException, IOException { | |||
delegate.receiveResponseEntity(response); | |||
// Read the entire response into a String | |||
StringWriter writer = new StringWriter(); | |||
final HttpEntity entity = response.getEntity(); | |||
final String entityData; | |||
if (entity != null && entity.getContent() != null) { | |||
@Cleanup final InputStreamReader in = new InputStreamReader(entity.getContent()); | |||
IOUtils.copyLarge(in, writer); | |||
entityData = writer.toString(); | |||
} else { | |||
entityData = null; | |||
} | |||
target.responseEntity(entityData); | |||
// Re-create the entity since we already read the entire InputStream | |||
response.setEntity(buildEntity(entityData)); | |||
} | |||
private BasicHttpEntity buildEntity(String entityData) { | |||
final BasicHttpEntity entity = new BasicHttpEntity(); | |||
entity.setContentLength(entityData == null ? 0 : entityData.length()); | |||
entity.setContent(new ByteArrayInputStream(entityData == null ? "".getBytes() : entityData.getBytes())); | |||
return entity; | |||
} | |||
} |
@@ -0,0 +1,22 @@ | |||
package org.cobbzilla.restex; | |||
import org.apache.http.config.ConnectionConfig; | |||
import org.apache.http.conn.HttpConnectionFactory; | |||
import org.apache.http.conn.ManagedHttpClientConnection; | |||
import org.apache.http.conn.routing.HttpRoute; | |||
import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory; | |||
public class RestexClientConnectionFactory implements HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> { | |||
private RestexClientConnectionManager connectionManager; | |||
private ManagedHttpClientConnectionFactory connectionFactory = new ManagedHttpClientConnectionFactory(); | |||
public RestexClientConnectionFactory(RestexClientConnectionManager connectionManager) { | |||
this.connectionManager = connectionManager; | |||
} | |||
@Override | |||
public ManagedHttpClientConnection create(HttpRoute route, ConnectionConfig config) { | |||
return new RestexClientConnection(connectionFactory.create(route, config), connectionManager.getTarget()); | |||
} | |||
} |
@@ -0,0 +1,35 @@ | |||
package org.cobbzilla.restex; | |||
import lombok.Delegate; | |||
import lombok.Getter; | |||
import org.apache.http.client.HttpClient; | |||
import org.apache.http.config.Registry; | |||
import org.apache.http.config.RegistryBuilder; | |||
import org.apache.http.conn.HttpClientConnectionManager; | |||
import org.apache.http.conn.socket.ConnectionSocketFactory; | |||
import org.apache.http.conn.socket.PlainConnectionSocketFactory; | |||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory; | |||
import org.apache.http.impl.client.HttpClientBuilder; | |||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager; | |||
public class RestexClientConnectionManager implements HttpClientConnectionManager { | |||
@Delegate private final BasicHttpClientConnectionManager delegate; | |||
@Getter private final RestexCaptureTarget target; | |||
private static Registry<ConnectionSocketFactory> getDefaultRegistry() { | |||
return RegistryBuilder.<ConnectionSocketFactory>create() | |||
.register("http", PlainConnectionSocketFactory.getSocketFactory()) | |||
.register("https", SSLConnectionSocketFactory.getSocketFactory()) | |||
.build(); | |||
} | |||
public RestexClientConnectionManager(RestexCaptureTarget target) { | |||
this.delegate = new BasicHttpClientConnectionManager(getDefaultRegistry(), new RestexClientConnectionFactory(this)); | |||
this.target = target; | |||
} | |||
public HttpClient getHttpClient () { | |||
return HttpClientBuilder.create().setConnectionManager(new RestexClientConnectionManager(target)).build(); | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
package org.cobbzilla.restex; | |||
import org.apache.http.conn.ConnectionPoolTimeoutException; | |||
import org.apache.http.conn.ConnectionRequest; | |||
import org.apache.http.conn.ManagedHttpClientConnection; | |||
import java.util.concurrent.ExecutionException; | |||
import java.util.concurrent.TimeUnit; | |||
public class RestexClientConnectionRequest implements ConnectionRequest { | |||
private final ConnectionRequest delegate; | |||
private final RestexCaptureTarget target; | |||
public RestexClientConnectionRequest(ConnectionRequest clientConnectionRequest, RestexCaptureTarget target) { | |||
this.delegate = clientConnectionRequest; | |||
this.target = target; | |||
} | |||
@Override public boolean cancel() { return delegate.cancel(); } | |||
@Override | |||
public ManagedHttpClientConnection get(long timeout, TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException { | |||
final ManagedHttpClientConnection connection = (ManagedHttpClientConnection) delegate.get(timeout, tunit); | |||
return new RestexClientConnection(connection, target); | |||
} | |||
} |
@@ -0,0 +1,92 @@ | |||
package org.cobbzilla.restex.targets; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.ToString; | |||
import org.cobbzilla.restex.RestexCaptureTarget; | |||
import java.io.IOException; | |||
import java.util.ArrayList; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
@ToString(of={"requestMethod", "requestUri", "requestEntity", "note"}) | |||
public class SimpleCaptureTarget implements RestexCaptureTarget { | |||
@Getter @Setter private int statusCode; | |||
@Getter @Setter private String reasonPhrase; | |||
@Getter @Setter private String protocolVersion; | |||
@Getter @Setter private String requestMethod; | |||
@Getter @Setter private String requestUri; | |||
@Getter @Setter private Map<String, String> requestHeaders = new LinkedHashMap<>(); | |||
@Getter @Setter private Map<String, String> responseHeaders = new LinkedHashMap<>(); | |||
@Getter @Setter private String requestEntity; | |||
@Getter @Setter private String responseEntity; | |||
@Getter @Setter private String binaryRequest; | |||
@Getter @Setter private String binaryResponse; | |||
@Getter @Setter private String note; | |||
public void appendNote (String n) { if (note == null) { note = n ; } else { note += "\n" + n; } } | |||
public List<String> getRequestHeaderList () { return buildHeaderList(requestHeaders); } | |||
public List<String> getResponseHeaderList () { return buildHeaderList(responseHeaders); } | |||
public String getResponseLine () { return statusCode + " " + (reasonPhrase == null ? "" : reasonPhrase) + " " + protocolVersion; } | |||
private List<String> buildHeaderList(Map<String, String> headers) { | |||
List<String> headerList = new ArrayList<>(); | |||
for (String header : headers.keySet()) { | |||
headerList.add(header + ": " + headers.get(header)); | |||
} | |||
return headerList; | |||
} | |||
@Override public void requestUri (String method, String uri) { | |||
setRequestMethod(method); | |||
setRequestUri(uri); | |||
} | |||
@Override public void requestHeader(String name, String value) { requestHeaders.put(name, value); } | |||
@Override public void requestEntity(String entityData) { | |||
if (binaryRequest != null) { | |||
requestEntity = "Binary request: " + binaryRequest; | |||
} else { | |||
requestEntity = entityData; | |||
} | |||
} | |||
@Override public void responseStatus(int statusCode, String reasonPhrase, String protocolVersion) { | |||
this.statusCode = statusCode; this.reasonPhrase = reasonPhrase; this.protocolVersion = protocolVersion; | |||
} | |||
@Override public void responseHeader(String name, String value) { responseHeaders.put(name, value); } | |||
@Override public void responseEntity(String entityData) { | |||
if (binaryResponse != null) { | |||
responseEntity = "Binary response: " + binaryResponse; | |||
} else { | |||
responseEntity = entityData; | |||
} | |||
} | |||
@Override public void commit() throws IOException {} | |||
public void reset () { | |||
requestUri = null; | |||
requestHeaders = new LinkedHashMap<>(); | |||
requestEntity = null; | |||
statusCode = -1; | |||
reasonPhrase = null; | |||
protocolVersion = null; | |||
responseHeaders = new LinkedHashMap<>(); | |||
responseEntity = null; | |||
} | |||
} |
@@ -0,0 +1,294 @@ | |||
package org.cobbzilla.restex.targets; | |||
import com.github.jknack.handlebars.Handlebars; | |||
import com.github.jknack.handlebars.Helper; | |||
import com.github.jknack.handlebars.Options; | |||
import com.github.jknack.handlebars.Template; | |||
import com.github.jknack.handlebars.io.ClassPathTemplateLoader; | |||
import lombok.AllArgsConstructor; | |||
import lombok.EqualsAndHashCode; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.restex.RestexCaptureTarget; | |||
import java.io.*; | |||
import java.util.*; | |||
@Slf4j | |||
public class TemplateCaptureTarget implements RestexCaptureTarget { | |||
public static final String SCOPE_HTTP = "http"; | |||
public static final String SCOPE_ANCHOR = "anchor"; | |||
public static final String SCOPE_FILES = "files"; | |||
public static final String HTML_SUFFIX = ".html"; | |||
public static final String FOOTER_START = "@@FOOTER@@"; | |||
public static final String INDEX_INSERTION_POINT = "@@MORE-INDEX-FILES@@"; | |||
public static final String DEFAULT_INDEX_TEMPLATE = "defaultIndex"; | |||
public static final String DEFAULT_INDEX_MORE_TEMPLATE = "defaultIndexMore"; | |||
public static final String DEFAULT_HEADER_TEMPLATE = "defaultHeader"; | |||
public static final String DEFAULT_FOOTER_TEMPLATE = "defaultFooter"; | |||
public static final String DEFAULT_ENTRY_TEMPLATE = "defaultEntry"; | |||
@Getter private File baseDir; | |||
private final Template indexTemplate; | |||
private final Template indexMoreTemplate; | |||
private final Template headerTemplate; | |||
private final Template footerTemplate; | |||
private final Template entryTemplate; | |||
private SortedSet<ContextFile> contextFiles = new TreeSet<>(); | |||
private Map<String, ContextFile> contextFileMap = new HashMap<>(); | |||
private final Set<File> filesOpen = new HashSet<>(); | |||
// current state, initialized in startRecording, reset in commit | |||
private SimpleCaptureTarget currentCapture = null; | |||
@Getter private final List<SimpleCaptureTarget> captures = new ArrayList<>(); | |||
private boolean recording = false; | |||
@Getter private String context = ""; | |||
@Getter private String comment = ""; | |||
public TemplateCaptureTarget (String baseDir) { | |||
this(new File(baseDir), DEFAULT_INDEX_TEMPLATE, DEFAULT_INDEX_MORE_TEMPLATE, DEFAULT_HEADER_TEMPLATE, DEFAULT_FOOTER_TEMPLATE, DEFAULT_ENTRY_TEMPLATE); | |||
} | |||
public TemplateCaptureTarget (File baseDir) { | |||
this(baseDir, DEFAULT_INDEX_TEMPLATE, DEFAULT_INDEX_MORE_TEMPLATE, DEFAULT_HEADER_TEMPLATE, DEFAULT_FOOTER_TEMPLATE, DEFAULT_ENTRY_TEMPLATE); | |||
} | |||
public TemplateCaptureTarget (File baseDir, | |||
String indexTemplate, | |||
String indexMoreTemplate, | |||
String headerTemplate, | |||
String footerTemplate, | |||
String entryTemplate) { | |||
this.baseDir = baseDir; | |||
if (!baseDir.exists() && !baseDir.mkdirs()) { | |||
throw new IllegalArgumentException("baseDir does not exist and could not be created: "+baseDir.getAbsolutePath()); | |||
} | |||
final Handlebars handlebars = new Handlebars(new ClassPathTemplateLoader("/")); | |||
handlebars.registerHelper("nl2br", new Helper<Object>() { | |||
public CharSequence apply(Object src, Options options) { | |||
return src == null || src.toString().isEmpty() ? "" : new Handlebars.SafeString(src.toString().replace("\n", "<br/>")); | |||
} | |||
}); | |||
this.indexTemplate = compileOrDie(indexTemplate, handlebars); | |||
this.indexMoreTemplate = compileOrDie(indexMoreTemplate, handlebars); | |||
this.headerTemplate = compileOrDie(headerTemplate, handlebars); | |||
this.footerTemplate = compileOrDie(footerTemplate, handlebars); | |||
this.entryTemplate = compileOrDie(entryTemplate, handlebars); | |||
} | |||
public Template compileOrDie(String template, Handlebars handlebars) { | |||
try { | |||
return handlebars.compile(template); | |||
} catch (IOException e) { | |||
throw new IllegalStateException("Error compiling template '"+ template+"': "+e, e); | |||
} | |||
} | |||
public synchronized void startRecording (String context, String comment) { | |||
if (recording) { | |||
log.warn("startRecording: cannot start "+context+"/"+comment+", already recording "+this.context+"/"+this.comment); | |||
try { | |||
if (!captures.isEmpty()) commit(); | |||
} catch (IOException e) { | |||
log.error("startRecording: error committing docs: "+e); | |||
} finally { | |||
reset(); | |||
} | |||
} | |||
this.context = context; | |||
this.comment = comment; | |||
currentCapture = new SimpleCaptureTarget(); | |||
recording = true; | |||
} | |||
@Override public void requestUri(String method, String uri) { | |||
if (currentCapture != null) currentCapture.requestUri(method, uri); | |||
} | |||
@Override public void requestHeader(String name, String value) { if (recording) currentCapture.requestHeader(name, value); } | |||
@Override public void requestEntity(String entityData) { if (recording) currentCapture.requestEntity(entityData); } | |||
@Override public void responseStatus(int statusCode, String reasonPhrase, String protocolVersion) { | |||
if (recording) currentCapture.responseStatus(statusCode, reasonPhrase, protocolVersion); | |||
} | |||
@Override public void responseHeader(String name, String value) { if (recording) currentCapture.responseHeader(name, value); } | |||
@Override public synchronized void responseEntity(String entityData) { | |||
if (recording) { | |||
currentCapture.responseEntity(entityData); | |||
captures.add(currentCapture); | |||
currentCapture = new SimpleCaptureTarget(); | |||
} | |||
} | |||
public void addNote (String note) { if (recording) currentCapture.appendNote(note); } | |||
@Override public void setBinaryRequest (String type) { if (recording) currentCapture.setBinaryRequest(type); } | |||
@Override public void setBinaryResponse (String type) { if (recording) currentCapture.setBinaryResponse(type); } | |||
public synchronized void commit () throws IOException { | |||
final String fileBaseName = context.replaceAll("[^A-Za-z0-9]", "_"); | |||
if (fileBaseName == null || fileBaseName.length() == 0) { | |||
log.warn("No context name set (not committing). currentCapture="+currentCapture); | |||
return; | |||
} | |||
final String uriFileName = fileBaseName + HTML_SUFFIX; | |||
ContextFile contextFile = contextFileMap.get(context); | |||
if (contextFile == null) { | |||
contextFile = new ContextFile(context, uriFileName); | |||
contextFileMap.put(context, contextFile); | |||
} | |||
contextFiles.add(contextFile); | |||
String anchor = comment.replaceAll("[^A-Za-z0-9]", "_"); | |||
contextFile.add(new ContextExample(anchor, comment)); | |||
final File uriFile = new File(baseDir, uriFileName); | |||
final boolean exists = uriFile.exists(); | |||
synchronized (filesOpen) { | |||
if (!exists) { | |||
// first time writing to the file, so write the header | |||
filesOpen.add(uriFile); | |||
try (FileWriter writer = new FileWriter(uriFile)) { | |||
render(headerTemplate, writer); | |||
} | |||
} else { | |||
// file exists -- have we written to it yet? | |||
if (!filesOpen.contains(uriFile)) { | |||
// file exists but we have not written to it, so rewrite the file without the footer | |||
removeFooter(uriFile); | |||
} | |||
} | |||
} | |||
try (FileWriter writer = new FileWriter(uriFile, true)) { | |||
// append the entry | |||
renderEntry(entryTemplate, writer, anchor); | |||
} | |||
reset(); | |||
} | |||
public synchronized void reset() { | |||
recording = false; | |||
context = ""; | |||
comment = ""; | |||
captures.clear(); | |||
currentCapture = null; | |||
} | |||
public synchronized void close () throws IOException { | |||
if (recording) commit(); | |||
for (File f : filesOpen) { | |||
try (FileWriter writer = new FileWriter(f, true)) { | |||
render(footerTemplate, writer); | |||
} | |||
} | |||
final File indexFile = new File(baseDir, "index.html"); | |||
if (!indexFile.exists()) { | |||
try (FileWriter writer = new FileWriter(indexFile)) { | |||
renderIndex(indexTemplate, writer); | |||
} | |||
} else { | |||
StringWriter writer = new StringWriter(); | |||
renderIndex(indexMoreTemplate, writer); | |||
replaceInFile(indexFile, INDEX_INSERTION_POINT, writer.toString()); | |||
} | |||
filesOpen.clear(); | |||
contextFiles.clear(); | |||
contextFileMap.clear(); | |||
} | |||
protected synchronized void apply(final Template template, Writer writer, Map<String, Object> scope) { | |||
synchronized (captures) { | |||
try { | |||
template.apply(scope, writer); | |||
} catch (IOException e) { | |||
throw new IllegalStateException("Error applying template '" + template.filename() + "': " + e, e); | |||
} | |||
} | |||
} | |||
protected void renderEntry(Template template, Writer writer, String anchor) { | |||
Map<String, Object> scope = new HashMap<>(); | |||
scope.put(SCOPE_HTTP, this); | |||
scope.put(SCOPE_ANCHOR, anchor); | |||
apply(template, writer, scope); | |||
} | |||
protected void render(Template template, Writer writer) { | |||
Map<String, Object> scope = new HashMap<>(); | |||
scope.put(SCOPE_HTTP, this); | |||
apply(template, writer, scope); | |||
} | |||
protected void renderIndex(Template template, Writer writer) { | |||
Map<String, Object> scope = new HashMap<>(); | |||
scope.put(SCOPE_FILES, contextFiles); | |||
apply(template, writer, scope); | |||
} | |||
private void removeFooter(File uriFile) throws IOException { | |||
File temp = File.createTempFile(getClass().getSimpleName(), HTML_SUFFIX, uriFile.getParentFile()); | |||
try (BufferedReader reader = new BufferedReader(new FileReader(uriFile))) { | |||
try (FileWriter writer = new FileWriter(temp)) { | |||
String line; | |||
while ((line = reader.readLine()) != null) { | |||
if (line.contains(FOOTER_START)) break; | |||
writer.write(line + "\n"); | |||
} | |||
} | |||
} | |||
if (!temp.renameTo(uriFile)) { | |||
throw new IllegalStateException("Error rewriting footer in file: "+uriFile.getAbsolutePath()); | |||
} | |||
} | |||
private void replaceInFile(File file, String insertionPoint, String data) throws IOException { | |||
File temp = File.createTempFile(getClass().getSimpleName(), HTML_SUFFIX, file.getParentFile()); | |||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) { | |||
try (FileWriter writer = new FileWriter(temp)) { | |||
String line; | |||
while ((line = reader.readLine()) != null) { | |||
if (line.contains(insertionPoint)) { | |||
writer.write(data); | |||
} | |||
writer.write(line + "\n"); | |||
} | |||
} | |||
} | |||
if (!temp.renameTo(file)) { | |||
throw new IllegalStateException("Error rewriting file: "+file.getAbsolutePath()); | |||
} | |||
} | |||
@AllArgsConstructor @EqualsAndHashCode(of="context") | |||
class ContextFile implements Comparable { | |||
@Getter @Setter public String context; | |||
@Getter @Setter public String fsPath; | |||
@Getter public final List<ContextExample> examples = new ArrayList<>(); | |||
public void add(ContextExample contextExample) { examples.add(contextExample); } | |||
@Override | |||
public int compareTo(Object o) { | |||
return (o instanceof ContextFile) ? context.compareTo(((ContextFile) o).getContext()) : 0; | |||
} | |||
} | |||
@AllArgsConstructor | |||
class ContextExample { | |||
@Getter @Setter public String anchor; | |||
@Getter @Setter public String description; | |||
} | |||
} |
@@ -0,0 +1,69 @@ | |||
<a name="{{anchor}}"></a> | |||
<div class="request_details" style="margin-top: 10px"><p class="title">TEST CASE: {{http.comment}}</p></div> | |||
<table> | |||
<thead> | |||
<tr><td class="summary" colspan="4">SUMMARY</td></tr> | |||
<tr> | |||
<td class="type_column">Type</td> | |||
<td >Request</td> | |||
<td class="response_column">Response</td> | |||
<td >Notes</td> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{{#each http.captures}} | |||
<tr> | |||
<td nowrap=""><div class="block margin_0">{{requestMethod}}</div></td> | |||
<td nowrap=""><a href="#{{anchor}}_{{@index}}">{{requestUri}}</a></td> | |||
<td nowrap="">{{responseLine}}</td> | |||
<td nowrap="">{{nl2br note}}</td> | |||
</tr> | |||
{{/each}} | |||
</tbody> | |||
</table> | |||
{{#each http.captures}} | |||
<a name="{{anchor}}_{{@index}}"></a> | |||
<div class="request_details"> | |||
<div class="block margin_0" style="margin-right: 5px">{{requestMethod}}</div>{{requestUri}} | |||
<div class="request_description">{{nl2br note}}</div> | |||
</div> | |||
<div class="request_response_container"> | |||
<p class="request">Request</p> | |||
<p class="item_description">header</p> | |||
<div class="request_header"> | |||
{{#each requestHeaderList}} | |||
<p >{{this}}</p> | |||
{{/each}} | |||
</div> | |||
<p class="item_description">body</p> | |||
<div class="request_body"> | |||
<pre>{{{requestEntity}}}</pre> | |||
</div> | |||
</div> | |||
<div class="request_response_container"> | |||
<p class="request">Response</p> | |||
<p class="item_description">header</p> | |||
<div class="request_header"> | |||
<p >{{responseLine}}</p> | |||
{{#each responseHeaderList}} | |||
<p>{{this}}<p/> | |||
{{/each}} | |||
</div> | |||
<p class="item_description">body</p> | |||
<div class="request_body"> | |||
<pre>{{{responseEntity}}}</pre> | |||
</div> | |||
</div> | |||
{{/each}} |
@@ -0,0 +1,3 @@ | |||
<!-- @@FOOTER@@ must be first line in footer file --> | |||
</body> | |||
</html> |
@@ -0,0 +1,147 @@ | |||
<html> | |||
<head> | |||
<title>Example HTTP exchanges for {{http.requestUri}}</title> | |||
<style type="text/css"> | |||
@font-face { | |||
font-family: "Kimberley"; | |||
src: url(http://www.princexml.com/fonts/larabie/ » | |||
kimberle.ttf) format("truetype"); | |||
} | |||
*{ | |||
font-family: "Arial", sans-serif | |||
} | |||
table{ | |||
min-width: 800px; | |||
} | |||
td{ | |||
vertical-align: middle; | |||
border: 1px solid #000000; | |||
border-width: 0px 0px 1px 0px; | |||
text-align: left; | |||
padding: 7px; | |||
font-size: 12px; | |||
font-family: Arial; | |||
font-weight: normal; | |||
color: #000000; | |||
} | |||
tr{ | |||
margin: 0; | |||
padding: 0; | |||
border: 0; | |||
font-size: 100%; | |||
font: inherit; | |||
} | |||
pre{ | |||
margin: 0px; | |||
font-size: 13px; | |||
} | |||
.border{ | |||
border: 1px solid gray; | |||
} | |||
.centered{ | |||
text-align: center | |||
} | |||
.block{ | |||
background-color: #03A2CC; | |||
display: inline-block; | |||
padding: 5px; | |||
border-radius: 5px; | |||
color: white; | |||
} | |||
.type_column{ | |||
width: 70px; | |||
} | |||
.response_column{ | |||
width: 100px; | |||
} | |||
.margin_0{ | |||
margin: 0px; | |||
} | |||
.summary{ | |||
font-size: 20px; | |||
color: #03A2CC;; | |||
} | |||
.request_details{ | |||
height: 28px; | |||
background-color: rgba(156, 156, 156, 0.48); | |||
padding: 10px; | |||
color: #6F6F6F; | |||
margin-top: 35px; | |||
margin-bottom: 25px; | |||
} | |||
.title{ | |||
margin: 3px; | |||
font-size: 20px; | |||
} | |||
.request_description{ | |||
display: inline-block; | |||
float: right; | |||
margin-top: 4px; | |||
} | |||
.request_header{ | |||
width: 500px; | |||
padding: 10px; | |||
color: white; | |||
background-color: #353535; | |||
margin-bottom: 10px; | |||
} | |||
.request_body{ | |||
width: 500px; | |||
padding: 10px; | |||
color: white; | |||
background-color: #353535; | |||
margin-bottom: 10px; | |||
} | |||
.request_header>p{ | |||
margin: 0px; | |||
font-size: 13px; | |||
} | |||
.request_body>p{ | |||
margin: 0px; | |||
font-size: 13px; | |||
} | |||
.item_description{ | |||
margin-bottom: 0px; | |||
} | |||
.request{ | |||
font-size: 20px; | |||
margin: 0px; | |||
} | |||
.request_response_container{ | |||
border: 1px solid rgba(158, 158, 158, 0.52); | |||
border-radius: 5px; | |||
padding: 10px; | |||
margin-bottom: 15px; | |||
} | |||
.main_container{ | |||
width: 800px; | |||
margin-left: auto; | |||
margin-right: auto; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
@@ -0,0 +1,85 @@ | |||
<html> | |||
<head> | |||
<title>Examples of all HTTP exchanges with this app</title> | |||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script> | |||
<style type="text/css"> | |||
a{ | |||
color: white; | |||
text-decoration: inherit; | |||
font-variant: small-caps; | |||
font-family: sans-serif; | |||
} | |||
.container{ | |||
width: 500px; | |||
padding: 10px; | |||
font-family: sans-serif; | |||
} | |||
.item{ margin: 10px; font-family: sans-serif;} | |||
.subItem{ margin: 5px; font-family: sans-serif;} | |||
.container li > a > span { | |||
float: right; | |||
font-size: 19px; | |||
font-weight: bolder; | |||
font-family: sans-serif; | |||
} | |||
.container li > a > span { | |||
width: 20px; | |||
height: 20px; | |||
text-align: center; | |||
font-family: sans-serif; | |||
} | |||
.container li.open > a > span:after { content: '\25b4'; } | |||
.container li > a > span:after { content: '\25be'; } | |||
.container li { list-style: none; } | |||
.ic{ margin: 5px; } | |||
.itemC{ | |||
margin: 5px; | |||
padding: 5px; | |||
padding-top: 9px; | |||
min-height: 22px; | |||
border-radius: 5px; | |||
background-color: #03A2CC; | |||
font-family: sans-serif; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<ul class="container"> | |||
{{#each files}} | |||
<li class="itemC"> | |||
<a class="item" href="{{fsPath}}"><span class="openItem"></span>{{context}}</a> | |||
<ul class="subItem"> | |||
{{#each examples}} | |||
<li class="ic"> | |||
<a href="{{fsPath}}#{{anchor}}">{{description}}</a> | |||
</li> | |||
{{/each}} | |||
</ul> | |||
</li> | |||
{{/each}} | |||
<!-- @@MORE-INDEX-FILES@@ --> | |||
</ul> | |||
</body> | |||
<script type="text/javascript"> | |||
$(document).ready(function () { | |||
var $subitem = $(".subitem"); | |||
$subitem.hide(); | |||
$('.itemC').removeClass('open'); | |||
$('.openItem').on("click", function (e) { | |||
e.preventDefault(); | |||
isOpened = $($(this).parent().parent()).hasClass('open'); | |||
$('.itemC').removeClass('open'); | |||
$subitem.hide(); | |||
if(!isOpened){ | |||
$('.subitem', $(this).parent().parent()).show(); | |||
$($(this).parent().parent()).addClass('open'); | |||
} | |||
}); | |||
}); | |||
</script> | |||
</html> |
@@ -0,0 +1,14 @@ | |||
{{#each files}} | |||
<li class="itemC"> | |||
<a class="item" href="{{fsPath}}"><span class="openItem"></span>{{context}}</a> | |||
<ul class="subItem"> | |||
{{#each examples}} | |||
<li class="ic"> | |||
<a href="{{fsPath}}#{{anchor}}">{{description}}</a> | |||
</li> | |||
{{/each}} | |||
</ul> | |||
</li> | |||
{{/each}} |
@@ -0,0 +1,135 @@ | |||
package org.cobbzilla.restex; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.apache.http.Header; | |||
import org.apache.http.HttpResponse; | |||
import org.apache.http.client.HttpClient; | |||
import org.apache.http.client.methods.HttpGet; | |||
import org.apache.http.message.BasicHeader; | |||
import org.cobbzilla.restex.targets.SimpleCaptureTarget; | |||
import org.cobbzilla.restex.targets.TemplateCaptureTarget; | |||
import org.eclipse.jetty.server.Request; | |||
import org.eclipse.jetty.server.Server; | |||
import org.eclipse.jetty.server.handler.AbstractHandler; | |||
import org.junit.After; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.nio.file.FileSystems; | |||
import java.nio.file.Files; | |||
import java.nio.file.Path; | |||
import java.util.Enumeration; | |||
import java.util.LinkedHashMap; | |||
import java.util.Map; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertTrue; | |||
@Slf4j | |||
public class RestexIT { | |||
public static final int TEST_PORT = 19091; | |||
private Server server; | |||
class RestexTestHandler extends AbstractHandler { | |||
@Override | |||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { | |||
// do nothing | |||
log.info("handle called."); | |||
response.setStatus(200); | |||
final Enumeration<String> headerNames = request.getHeaderNames(); | |||
while (headerNames.hasMoreElements()) { | |||
String headerName = headerNames.nextElement(); | |||
response.addHeader(headerName, request.getHeader(headerName)); | |||
} | |||
response.getWriter().write("foo"); | |||
response.getWriter().flush(); | |||
response.setContentLength("foo".length()); | |||
} | |||
} | |||
@Before | |||
public void setUp () throws Exception { | |||
server = new Server(TEST_PORT); | |||
server.setHandler(new RestexTestHandler()); | |||
server.start(); | |||
} | |||
@After public void tearDown () throws Exception { server.stop(); } | |||
@Test | |||
public void testSimpleCapture () throws Exception { | |||
RestexCaptureTarget target = new SimpleCaptureTarget(); | |||
HttpClient httpClient = new RestexClientConnectionManager(target).getHttpClient(); | |||
HttpGet httpGet = new HttpGet("http://127.0.0.1:"+TEST_PORT+"/test"); | |||
String requestHeaderName1 = "foo-"+System.currentTimeMillis(); | |||
String requestHeaderValue1 = "bar-"+System.currentTimeMillis(); | |||
httpGet.addHeader(new BasicHeader(requestHeaderName1, requestHeaderValue1)); | |||
final HttpResponse response = httpClient.execute(httpGet); | |||
final Map<String, String> responseHeaderMap = buildResponseHeaderMap(response); | |||
assertTrue("didn't find response header", responseHeaderMap.containsKey(requestHeaderName1)); | |||
assertEquals("wrong request header value for "+requestHeaderName1, requestHeaderValue1, responseHeaderMap.get(requestHeaderName1)); | |||
} | |||
@Test | |||
public void testTemplateCapture () throws Exception { | |||
File tempDir = createTempDir(new File(System.getProperty("java.io.tmpdir")), getClass().getSimpleName()); | |||
log.info("Writing to tempDir="+tempDir.getAbsolutePath()); | |||
TemplateCaptureTarget target = new TemplateCaptureTarget(tempDir, "testTemplateIndex", "testTemplateIndexMore", "testTemplateHeader", "testTemplateFooter", "testTemplateEntry"); | |||
HttpClient httpClient = new RestexClientConnectionManager(target).getHttpClient(); | |||
String header1; | |||
String value1; | |||
HttpGet httpGet; | |||
HttpResponse response; | |||
target.startRecording("test1", "a single request of /test goes like this"); | |||
httpGet = new HttpGet("http://127.0.0.1:"+TEST_PORT+"/test"); | |||
header1 = "foo-"+System.currentTimeMillis(); | |||
value1 = "bar-"+System.currentTimeMillis(); | |||
httpGet.addHeader(new BasicHeader(header1, value1)); | |||
response = httpClient.execute(httpGet); | |||
httpGet.releaseConnection(); | |||
target.commit(); | |||
target.startRecording("test2", "a request of /test and then /test2 goes like this"); | |||
httpGet = new HttpGet("http://127.0.0.1:"+TEST_PORT+"/test"); | |||
header1 = "foo2-"+System.currentTimeMillis(); | |||
value1 = "bar2-"+System.currentTimeMillis(); | |||
httpGet.addHeader(new BasicHeader(header1, value1)); | |||
response = httpClient.execute(httpGet); | |||
httpGet.releaseConnection(); | |||
httpGet = new HttpGet("http://127.0.0.1:"+TEST_PORT+"/test2"); | |||
header1 = "foo-"+System.currentTimeMillis(); | |||
value1 = "bar-"+System.currentTimeMillis(); | |||
httpGet.addHeader(new BasicHeader(header1, value1)); | |||
response = httpClient.execute(httpGet); | |||
httpGet.releaseConnection(); | |||
target.close(); | |||
} | |||
private Map<String, String> buildResponseHeaderMap(HttpResponse response) { | |||
Map<String, String> headers = new LinkedHashMap<>(); | |||
for (Header header : response.getAllHeaders()) { | |||
headers.put(header.getName(), header.getValue()); | |||
} | |||
return headers; | |||
} | |||
public static File createTempDir(File parentDir, String prefix) throws IOException { | |||
Path parent = FileSystems.getDefault().getPath(parentDir.getAbsolutePath()); | |||
return new File(Files.createTempDirectory(parent, prefix).toAbsolutePath().toString()); | |||
} | |||
} |
@@ -0,0 +1,20 @@ | |||
<hr/> | |||
<h2>{{http.comment}}</h2> | |||
<h3>Request</h3> | |||
<em>{{http.requestMethod}} {{http.requestUri}}</em> | |||
{{#each http.requestHeaderList}} | |||
{{this}}<br/> | |||
{{/each}} | |||
<br/> | |||
{{http.requestEntity}} | |||
<h3>Response</h3> | |||
{{http.responseLine}} | |||
{{#each http.responseHeaderList}} | |||
{{this}}<br/> | |||
{{/each}} | |||
<br/> | |||
{{http.responseEntity}} | |||
@@ -0,0 +1,3 @@ | |||
<!-- @@FOOTER@@ must be first line in footer file --> | |||
</body> | |||
</html> |
@@ -0,0 +1,6 @@ | |||
<html> | |||
<head> | |||
<title>Example HTTP exchanges for {{http.requestUri}}</title> | |||
</head> | |||
<body> | |||
@@ -0,0 +1,15 @@ | |||
<html> | |||
<head> | |||
<title>Examples of all HTTP exchanges with this app</title> | |||
</head> | |||
<body> | |||
<ul> | |||
{{#each files}} | |||
<li><a href="{{fsPath}}">{{context}}</a></li> | |||
{{/each}} | |||
<!-- @@MORE-INDEX-FILES@@ --> | |||
</ul> | |||
</body> | |||
</html> |
@@ -0,0 +1,3 @@ | |||
{{#each files}} | |||
<li><a href="{{fsPath}}">{{context}}</a></li> | |||
{{/each}} |