@@ -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}} |