Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add Caldecott Java client

- Add library module for Caldecott Java client

- Add test module for integration tests for Caldecott Java client

- Add new profiles to CloudFoundry Java client to exclude integration test by default

- Add integration-test profile that executes all integration tests

Change-Id: I70437d5ab807fa307275f7007b2b7f858e80a10e
  • Loading branch information...
commit 1930bcddf043d4a2e525f67a314963ad8b118c3a 1 parent 0acb855
Thomas Risberg authored
Showing with 5,544 additions and 21 deletions.
  1. +2 −0  .gitignore
  2. +4 −0 cloudfoundry-caldecott-lib/.gitignore
  3. +15 −0 cloudfoundry-caldecott-lib/README
  4. +135 −0 cloudfoundry-caldecott-lib/pom.xml
  5. +33 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/TunnelException.java
  6. +35 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/Client.java
  7. +215 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/HttpTunnel.java
  8. +61 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/HttpTunnelFactory.java
  9. +80 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/SocketClient.java
  10. +32 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/Tunnel.java
  11. +116 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/TunnelAcceptor.java
  12. +28 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/TunnelFactory.java
  13. +172 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/TunnelHandler.java
  14. +162 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/TunnelHelper.java
  15. +120 −0 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/TunnelServer.java
  16. +35 −0 cloudfoundry-caldecott-lib/src/main/resources/log4j.xml
  17. +139 −0 cloudfoundry-caldecott-lib/src/test/java/org/cloudfoundry/caldecott/JavaTunnel.java
  18. +114 −0 cloudfoundry-caldecott-lib/src/test/java/org/cloudfoundry/caldecott/client/HttpTunnelTest.java
  19. +97 −0 cloudfoundry-caldecott-lib/src/test/java/org/cloudfoundry/caldecott/client/SocketClientTest.java
  20. +1 −0  cloudfoundry-caldecott-lib/tunnel.sh
  21. +2 −0  cloudfoundry-caldecott-test/.gitignore
  22. +1,602 −0 cloudfoundry-caldecott-test/data/load.json
  23. +1,611 −0 cloudfoundry-caldecott-test/data/load.xml
  24. +206 −0 cloudfoundry-caldecott-test/pom.xml
  25. +437 −0 cloudfoundry-caldecott-test/src/test/java/org/cloudfoundry/caldecott/client/HttpTunnelTest.java
  26. +29 −0 cloudfoundry-caldecott-test/src/test/resources/log4j.xml
  27. +37 −15 cloudfoundry-client-lib/pom.xml
  28. +2 −1  cloudfoundry-maven-plugin/.gitignore
  29. +22 −5 pom.xml
View
2  .gitignore
@@ -7,3 +7,5 @@ build/
.classpath
.project
.settings
+/*.DS_Store
+/*.iml
View
4 cloudfoundry-caldecott-lib/.gitignore
@@ -0,0 +1,4 @@
+/out/
+/caldecott.log*
+/target/
+/*.iml
View
15 cloudfoundry-caldecott-lib/README
@@ -0,0 +1,15 @@
+Java client for Caldecott
+
+This library is intended to be used from a Java application when tunneling into Cloud Foundry data services using the server side Caldecott application. It is similar to the tunnel feature of the command line client vmc. Once the tunnel is started you can connect with a local data client application using the appropriate connection parameters. This library provides functionality to run as a server for a specific Cloud Foundry data service. This server will listen for local connections on a specific port to provide tunneling via the server side Caldecott application.
+
+There is a Java class (JavaTunnel.java) under the src/test directory that starts up a tunnel server and will prompt for connection information needed. It offers equivalent functionality as 'vmc tunnel'. Currently such usage is intended for testing purpose. This JavaTunnel.java class also shows how to setup and start a tunnel so it can be used as a guide when building your own client code.
+
+How to run this Java tunnel program:
+
+You can use the 'tunnel.sh' shell script that uses the mvn exec target. You can provide the email to log in with using a system variable called vcap.email (example: -Dvcap.email=me@mycompany.com). Optionally override the VCAP target using vcap.target with a specific cloud url to run against a local cloud (example: -Dvcap.target=http://api.vcap.me).
+
+To build and run using Maven simply use:
+ mvn clean install
+ mvn --quiet exec:java -Dexec.mainClass="org.cloudfoundry.caldecott.JavaTunnel" -Dexec.classpathScope="test"
+
+NOTE: This implementation doesn't deploy the server side Caldecott app at this point so you should test the tunnel with 'vmc tunnel' first.
View
135 cloudfoundry-caldecott-lib/pom.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.cloudfoundry</groupId>
+ <artifactId>cloudfoundry-caldecott-lib</artifactId>
+ <version>0.1.0.BUILD-SNAPSHOT</version>
+ <packaging>jar</packaging>
+ <name>Client library to be used for Caldecott access</name>
+
+ <licenses>
+ <license>
+ <name>Apache 2.0 License</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <properties>
+ <spring.framework.version>3.0.5.RELEASE</spring.framework.version>
+ <cf.client.lib.version>0.7.2.BUILD-SNAPSHOT</cf.client.lib.version>
+ <junit.version>4.8.2</junit.version>
+ <mockito.version>1.8.5</mockito.version>
+ </properties>
+
+ <profiles>
+ <profile>
+ <id>fast</id>
+ <properties>
+ <maven.test.skip>true</maven.test.skip>
+ </properties>
+ </profile>
+ </profiles>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-web</artifactId>
+ <version>${spring.framework.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.cloudfoundry</groupId>
+ <artifactId>cloudfoundry-client-lib</artifactId>
+ <version>${cf.client.lib.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-core-asl</artifactId>
+ <version>1.6.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-mapper-asl</artifactId>
+ <version>1.6.2</version>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>${junit.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <version>${mockito.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-test</artifactId>
+ <version>${spring.framework.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ <version>1.2.14</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.1</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <extensions>
+ <extension>
+ <groupId>org.springframework.build.aws</groupId>
+ <artifactId>org.springframework.build.aws.maven</artifactId>
+ <version>3.0.0.RELEASE</version>
+ </extension>
+ </extensions>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.5</source>
+ <target>1.5</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.8</version>
+ <configuration>
+ <!--forkMode>pertest</forkMode-->
+ <includes>
+ <include>**/*Test.java</include>
+ </includes>
+ <excludes>
+ <exclude>**/Abstract*.java</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <distributionManagement>
+ <repository>
+ <id>spring-milestone</id>
+ <name>Spring Milestone Repository</name>
+ <url>s3://maven.springframework.org/milestone</url>
+ </repository>
+ <snapshotRepository>
+ <id>spring-snapshot</id>
+ <name>Spring Snapshot Repository</name>
+ <url>s3://maven.springframework.org/snapshot</url>
+ </snapshotRepository>
+ </distributionManagement>
+
+</project>
View
33 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/TunnelException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott;
+
+/**
+ * Exception thrown as the result of an error condition during tunnel communications.
+ *
+ * @author Thomas Risberg
+ */
+public class TunnelException extends RuntimeException {
+
+ public TunnelException(String message) {
+ super(message);
+ }
+
+ public TunnelException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+}
View
35 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/Client.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+import java.io.IOException;
+
+/**
+ * The interface defining the client SPI.
+ *
+ * @author Thomas Risberg
+ */
+public interface Client {
+
+
+ byte[] read() throws IOException;
+
+ void write(byte[] data) throws IOException;
+
+ boolean isActive();
+
+}
View
215 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/HttpTunnel.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.RequestCallback;
+import org.springframework.web.client.ResponseExtractor;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The Http implementation of a Tunnel designed to interact with the Caldecott server REST application.
+ *
+ * @author Thomas Risberg
+ */
+public class HttpTunnel implements Tunnel {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ // configuration options for the tunnel
+ private String url;
+ private String host;
+ private int port;
+ private String auth;
+
+ // REST template to use for tunnel communication
+ private final RestOperations restOperations;
+
+ // variables to keep track of communication state with the tunnel web service
+ private Map<String, String> tunnelInfo;
+ private long lastWrite = 0;
+ private long lastRead = 0;
+
+ public HttpTunnel(String url, String host, int port, String auth) {
+ this(url, host, port, auth, new RestTemplate());
+ }
+
+ public HttpTunnel(String url, String host, int port, String auth, RestOperations restOperations) {
+ this.url = url;
+ this.host = host;
+ this.port = port;
+ this.auth = auth;
+ this.restOperations = restOperations;
+ openTunnel();
+ }
+
+ public void write(byte[] data) {
+ sendBytes(data, ++lastWrite);
+ }
+
+ public byte[] read(boolean retry) {
+ if (!retry) {
+ lastRead++;
+ }
+ return receiveBytes(lastRead);
+ }
+
+ private void openTunnel() {
+ String initMsg = "{\"host\":\"" + host + "\",\"port\":" + port + "}";
+ if (logger.isDebugEnabled()) {
+ logger.debug("Initializing tunnel: " + initMsg);
+ }
+ HttpHeaders requestHeaders = new HttpHeaders();
+ requestHeaders.set("Auth-Token", auth);
+ requestHeaders.set("Content-Length", initMsg.length()+"");
+ HttpEntity<String> requestEntity = new HttpEntity<String>(initMsg, requestHeaders);
+ String jsonResponse = restOperations.postForObject(url + "/tunnels", requestEntity, String.class);
+ try {
+ this.tunnelInfo = TunnelHelper.convertJsonToMap(jsonResponse);
+ } catch (IOException ignore) {
+ this.tunnelInfo = new HashMap<String, String>();
+ }
+ }
+
+ public void close() {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Deleting tunnel " + this.tunnelInfo.get("path"));
+ }
+ HttpHeaders requestHeaders = new HttpHeaders();
+ requestHeaders.set("Auth-Token", auth);
+ HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
+ try {
+ restOperations.exchange(url + this.tunnelInfo.get("path"), HttpMethod.DELETE, requestEntity, null);
+ } catch (HttpClientErrorException e) {
+ if (e.getStatusCode().value() == 404) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Tunnel not found [" + e.getStatusCode() + "] " + e.getStatusText());
+ }
+ }
+ else {
+ logger.warn("Error while deleting tunnel [" + e.getStatusCode() + "] " + e.getStatusText());
+ }
+ }
+ }
+
+ private void sendBytes(byte[] bytes, long page) {
+ HttpHeaders requestHeaders = new HttpHeaders();
+ requestHeaders.set("Auth-Token", auth);
+ requestHeaders.set("Content-Length", bytes.length+"");
+ String dataUrl = url + this.tunnelInfo.get("path_in") + "/" + page;
+ HttpEntity<byte[]> requestEntity = new HttpEntity<byte[]>(bytes, requestHeaders);
+ if (logger.isTraceEnabled())
+ logger.trace("SENDING: " + printBytes(bytes));
+ ResponseEntity<?> response = restOperations.exchange(dataUrl, HttpMethod.PUT, requestEntity, null);
+ if (logger.isDebugEnabled()) {
+ logger.debug("[" + bytes.length + " bytes] PUT to " + dataUrl +" resulted in: " + response.getStatusCode());
+ }
+ }
+
+ private byte[] receiveBytes(long page) {
+ byte[] response = receiveDataBuffered(page);
+ if (logger.isTraceEnabled())
+ logger.trace("RECEIVED: " + printBytes(response));
+ return response;
+ }
+
+ private byte[] receiveDataBuffered(long page) {
+ final String dataUrl = url + this.tunnelInfo.get("path_out") + "/" + page;
+ byte[] responseBytes;
+ try {
+ responseBytes = restOperations.execute(
+ dataUrl,
+ HttpMethod.GET,
+ new RequestCallback() {
+ public void doWithRequest(ClientHttpRequest clientHttpRequest) throws IOException {
+ clientHttpRequest.getHeaders().set("Auth-Token", auth);
+ }
+ },
+ new ResponseExtractor<byte[]>() {
+ public byte[] extractData(ClientHttpResponse clientHttpResponse) throws IOException {
+ if (logger.isDebugEnabled())
+ logger.debug("HEADER: " + clientHttpResponse.getHeaders().toString());
+ int length = (int)clientHttpResponse.getHeaders().getContentLength();
+ InputStream stream = clientHttpResponse.getBody();
+ byte[] bytes = new byte[length];
+ int bytesRead = 0;
+ while (bytesRead < length) {
+ int r = stream.read(bytes, bytesRead, length - bytesRead);
+ if (r < 0) {
+ logger.warn("End of stream received from GET from " + dataUrl + " - we have read " + bytesRead + " bytes of " + length + " total");
+ break;
+ }
+ bytesRead = bytesRead + r;
+ if (logger.isTraceEnabled())
+ logger.trace("Have read " + r + " bytes which makes " + bytesRead + " of " + length + " completed");
+ }
+ if (logger.isDebugEnabled()) {
+ logger.debug("[" + length + " bytes] GET from " + dataUrl + " resulted in: " + clientHttpResponse.getStatusCode());
+ }
+ return bytes;
+ }
+ }
+ );
+ } catch (HttpStatusCodeException e) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("GET from " + dataUrl + " resulted in: " + e.getStatusCode().value());
+ }
+ throw e;
+ }
+ return responseBytes;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpTunnel for " + url + " on " + host + ":" + port;
+ }
+
+ private static String printBytes(byte[] array) {
+ StringBuilder printable = new StringBuilder();
+ printable.append("[" + array.length + "] = " + "0x");
+ for (int k = 0; k < array.length; k++) {
+ printable.append(byteToHex(array[k]));
+ }
+ return printable.toString();
+ }
+
+ private static String byteToHex(byte b) {
+ // Returns hex String representation of byte b
+ char hexDigit[] = {
+ '0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+ };
+ char[] array = {hexDigit[(b >> 4) & 0x0f], hexDigit[b & 0x0f]};
+ return new String(array);
+ }
+
+}
View
61 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/HttpTunnelFactory.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.web.client.RestOperations;
+
+/**
+ * The factory class used to create an HttpTunnel instance.
+ *
+ * @author Thomas Risberg
+ */
+public class HttpTunnelFactory implements TunnelFactory {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private final String url;
+ private final String host;
+ private final int port;
+ private final String auth;
+ private RestOperations restOperations;
+
+ public HttpTunnelFactory(String url, String host, int port, String auth) {
+ this(url, host, port, auth, null);
+ }
+
+ public HttpTunnelFactory(String url, String host, int port, String auth, RestOperations restOperations) {
+ this.url = url;
+ this.host = host;
+ this.port = port;
+ this.auth = auth;
+ this.restOperations = restOperations;
+ }
+
+ public Tunnel createTunnel() {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Creating HttpTunnel for " + url + " on " + host + ":" + port);
+ }
+ if (restOperations!= null) {
+ return new HttpTunnel(url, host, port, auth, restOperations);
+ }
+ else {
+ return new HttpTunnel(url, host, port, auth);
+ }
+ }
+}
View
80 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/SocketClient.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.Arrays;
+
+/**
+ * The Socket implementation of a Client designed to interact with data access clients.
+ *
+ * @author Thomas Risberg
+ */
+public class SocketClient implements Client {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ // configuration options for the socket
+ private final Socket socket;
+
+ // variables to keep track of communication state with the client
+ private boolean active = true;
+
+ public SocketClient(Socket socket) {
+ this.socket = socket;
+ }
+
+ public byte[] read() throws IOException {
+ byte[] bytes = new byte[1024];
+ int len;
+ len = socket.getInputStream().read(bytes);
+ if (len < 0) {
+ if (len < 0) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("[" + len + "] detected closed stream");
+ }
+ active = false;
+ }
+ len = 0;
+ }
+ else {
+ if (logger.isTraceEnabled()) {
+ logger.trace("[" + len + " bytes] read from stream");
+ }
+ }
+ return Arrays.copyOfRange(bytes, 0, len);
+ }
+
+ public void write(byte[] data) throws IOException {
+ OutputStream s = socket.getOutputStream();
+ s.write(data);
+ s.flush();
+ if (logger.isTraceEnabled()) {
+ logger.trace("[" + data.length + " bytes] written to stream");
+ }
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+}
View
32 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/Tunnel.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+/**
+ * The interface defining the tunnel SPI.
+ *
+ * @author Thomas Risberg
+ */
+public interface Tunnel {
+
+ void write(byte[] data);
+
+ byte[] read(boolean retry);
+
+ public void close();
+
+}
View
116 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/TunnelAcceptor.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.cloudfoundry.caldecott.TunnelException;
+import org.springframework.core.task.TaskExecutor;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.util.HashSet;
+import java.util.Observable;
+import java.util.Observer;
+
+/**
+ * The class responsible for listening for client connection attempts and handing off to
+ * a TunnelHandler for handling the actual tunneling communications.
+ *
+ * @author Thomas Risberg
+ */
+public class TunnelAcceptor implements Runnable {
+
+ public static final int SOCKET_TIMEOUT = 10000;
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ // configuration options
+ private final TunnelFactory tunnelFactory;
+ private final ServerSocket serverSocket;
+ private final TaskExecutor taskExecutor;
+
+ private volatile boolean keepGoing = true;
+
+ private volatile HashSet<Observable> handlers = new HashSet<Observable>();
+
+ public TunnelAcceptor(ServerSocket serverSocket, TunnelFactory tunnelFactory, TaskExecutor taskExecutor) {
+ this.serverSocket = serverSocket;
+ this.tunnelFactory = tunnelFactory;
+ this.taskExecutor = taskExecutor;
+ try {
+ this.serverSocket.setSoTimeout(SOCKET_TIMEOUT);
+ } catch (SocketException ignore) {}
+ }
+
+ public void start() {
+ logger.info("Starting new acceptor thread: " + this);
+ taskExecutor.execute(this);
+ logger.debug("Completed start of: " + taskExecutor);
+ }
+
+ public boolean isActive() {
+ if (handlers.size() > 0) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ public void stop() {
+ logger.info("Stop requested for: " + this);
+ keepGoing = false;
+ }
+
+ public void run() {
+ while (keepGoing) {
+ try {
+ logger.debug("Waiting for client connection");
+ Socket sourceSocket = serverSocket.accept();
+ logger.debug("Accepted client connection");
+ TunnelHandler handler = new TunnelHandler(sourceSocket, tunnelFactory, taskExecutor);
+ handler.addObserver(new Observer() {
+ public void update(Observable observable, Object o) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Notified that " + observable + " is now " + o);
+ }
+ handlers.remove(observable);
+ }
+ });
+ handlers.add(handler);
+ handler.start();
+ }
+ catch (SocketTimeoutException ste) {}
+ catch (IOException e) {
+ throw new TunnelException("Error while accepting connections", e);
+ }
+ }
+ if (!handlers.isEmpty()) {
+ logger.debug("Waiting for clients to close");
+ while (!handlers.isEmpty()) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException ignore) {}
+ }
+ }
+ logger.info("Completed acceptor thread for: " + this);
+ }
+}
View
28 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/TunnelFactory.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+/**
+ * The interface defining the TunnelFactory SPI.
+ *
+ * @author Thomas Risberg
+ */
+public interface TunnelFactory {
+
+ Tunnel createTunnel();
+
+}
View
172 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/TunnelHandler.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.cloudfoundry.caldecott.TunnelException;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.web.client.HttpStatusCodeException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.Observable;
+
+/**
+ * The class responsible for handling the actual tunneling communications between a data access client and the
+ * Caldecott server app.
+ *
+ * @author Thomas Risberg
+ */
+public class TunnelHandler extends Observable {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ // configuration options
+ private final Socket socket;
+ private final TunnelFactory tunnelFactory;
+ private final TaskExecutor taskExecutor;
+
+ // variables to keep state for the tunnel setup
+ private Client client;
+ private Tunnel tunnel;
+
+
+ public TunnelHandler(Socket socket, TunnelFactory tunnelFactory, TaskExecutor taskExecutor) {
+ this.socket = socket;
+ this.tunnelFactory = tunnelFactory;
+ this.taskExecutor = taskExecutor;
+ try {
+ this.socket.setSoTimeout(0);
+ } catch (SocketException ignore) {}
+ }
+
+ public void start() {
+ client = new SocketClient(socket);
+ tunnel = tunnelFactory.createTunnel();
+ taskExecutor.execute(new Writer());
+ taskExecutor.execute(new Reader());
+ if (logger.isDebugEnabled()) {
+ logger.debug("Completed start of: " + this.getClass().getSimpleName() + " with " + countObservers() + " observers");
+ }
+ }
+
+ public void stop() {
+ try {
+ InputStream is = socket.getInputStream();
+ if (is != null) {
+ is.close();
+ }
+ } catch (IOException ignore) {}
+ try {
+ OutputStream os = socket.getOutputStream();
+ if (os != null) {
+ os.close();
+ }
+ } catch (IOException ignore) {}
+ try {
+ socket.close();
+ } catch (IOException e) {
+ logger.warn("Error while closing client socket" + e.getMessage());
+ }
+ if (logger.isDebugEnabled()) {
+ logger.debug("Closing tunnel: " + tunnel.toString());
+ }
+ tunnel.close();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Notifying observers: " + countObservers());
+ }
+ setChanged();
+ notifyObservers("CLOSED");
+ }
+
+
+ private class Writer implements Runnable {
+
+ public void run() {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Starting new writer thread: " + this);
+ }
+ try {
+ while (client.isActive()) {
+ byte[] in = client.read();
+ if (in.length > 0) {
+ tunnel.write(in);
+ }
+ }
+ } catch (IOException e) {
+ throw new TunnelException("Error while processing streams", e);
+ }
+ stop();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Completed writer thread for: " + this);
+ }
+ }
+
+ }
+
+ private class Reader implements Runnable {
+ public void run() {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Starting new reader thread: " + this);
+ }
+ boolean retry = false;
+ try {
+ while (client.isActive()) {
+ try {
+ byte[] out = tunnel.read(retry);
+ retry = false;
+ client.write(out);
+ } catch (HttpStatusCodeException hsce) {
+ if (hsce.getStatusCode().value() == 504) {
+ retry = true;
+ if (logger.isTraceEnabled()) {
+ logger.trace("Retrying tunnel read after receiving " + hsce.getStatusCode().value());
+ }
+ }
+ else if (hsce.getStatusCode().value() == 404) {
+ retry = false;
+ if (logger.isDebugEnabled()) {
+ logger.debug("Tunnel error - [" + hsce.getStatusCode().value() + "] " + hsce.getStatusText());
+ }
+ }
+ else if (hsce.getStatusCode().value() == 410) {
+ retry = false;
+ if (logger.isDebugEnabled()) {
+ logger.debug("Tunnel error - [" + hsce.getStatusCode().value() + "] " + hsce.getStatusText());
+ }
+ }
+ else {
+ logger.warn("Received HTTP Error: [" + hsce.getStatusCode().value() + "] " + hsce.getStatusText());
+ throw new TunnelException("Error while reading from tunnel", hsce);
+ }
+ }
+ }
+ } catch (IOException ioe) {
+ throw new TunnelException("Error while processing streams", ioe);
+ }
+ if (logger.isDebugEnabled()) {
+ logger.debug("Completed reader thread for: " + this);
+ }
+ }
+
+ }
+
+}
View
162 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/TunnelHelper.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+import org.cloudfoundry.client.lib.CloudApplication;
+import org.cloudfoundry.client.lib.CloudFoundryClient;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.map.type.TypeFactory;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.client.ResourceAccessException;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A utility class for accessing information regarding tunnels and data services.
+ *
+ * @author Thomas Risberg
+ */
+public class TunnelHelper {
+
+ private static final String TUNNEL_APP_NAME = "caldecott";
+ private static final String[] TUNNEL_URI_SCHEMES = {"https:", "http:"};
+ private static final String TUNNEL_AUTH_KEY = "CALDECOTT_AUTH";
+ private static final Map<String, String> TUNNEL_URI_CACHE = new ConcurrentHashMap<String, String>();
+
+ private static final RestTemplate restTemplate = new RestTemplate();
+
+ private static ObjectMapper objectMapper = new ObjectMapper();
+
+ public static String getTunnelAppName() {
+ return TUNNEL_APP_NAME;
+ }
+
+ public static CloudApplication getTunnelAppInfo(CloudFoundryClient client) {
+ return client.getApplication(TunnelHelper.getTunnelAppName());
+ }
+
+ public static void bindServiceToTunnelApp(CloudFoundryClient client, String serviceName) {
+ if (getTunnelAppInfo(client).getServices().contains(serviceName)) {
+ return;
+ }
+ client.stopApplication(getTunnelAppName());
+ client.bindService(getTunnelAppName(), serviceName);
+ client.startApplication(getTunnelAppName());
+ }
+
+ public static String getTunnelUri(CloudFoundryClient client) {
+ String uriAuthority = client.getApplication(TunnelHelper.getTunnelAppName()).getUris().get(0);
+ if (TUNNEL_URI_CACHE.containsKey(uriAuthority)) {
+ return TUNNEL_URI_CACHE.get(uriAuthority);
+ }
+ String uriScheme = testUriSchemes(client, TUNNEL_URI_SCHEMES, uriAuthority);
+ String uri = uriScheme + "//" + uriAuthority;
+ TUNNEL_URI_CACHE.put(uriAuthority, uri);
+ return uri;
+ }
+
+ public static String getTunnelAuth(CloudFoundryClient client) {
+ String auth = client.getApplication(TunnelHelper.getTunnelAppName()).getEnvAsMap().get(TUNNEL_AUTH_KEY);
+ return auth;
+ }
+
+ public static Map<String, String> getTunnelServiceInfo(CloudFoundryClient client, String serviceName) {
+ String urlToUse = getTunnelUri(client) + "/services/" + serviceName;
+ HttpHeaders requestHeaders = new HttpHeaders();
+ requestHeaders.set("Auth-Token", getTunnelAuth(client));
+ HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
+ HttpEntity<String> response = restTemplate.exchange(urlToUse, HttpMethod.GET, requestEntity, String.class);
+ String json = response.getBody().trim();
+ Map<String, String> svcInfo = new HashMap<String, String>();
+ try {
+ svcInfo = convertJsonToMap(json);
+ } catch (IOException e) {
+ return new HashMap<String, String>();
+ }
+ if (svcInfo.containsKey("url")) {
+ String svcUrl = svcInfo.get("url");
+ try {
+ URI uri = new URI(svcUrl);
+ String[] userInfo;
+ if (uri.getUserInfo().contains(":")) {
+ userInfo = uri.getUserInfo().split(":");
+ }
+ else {
+ userInfo = new String[2];
+ userInfo[0] = uri.getUserInfo();
+ userInfo[1] = "";
+ }
+ svcInfo.put("user", userInfo[0]);
+ svcInfo.put("username", userInfo[0]);
+ svcInfo.put("password", userInfo[1]);
+ svcInfo.put("host", uri.getHost());
+ svcInfo.put("hostname", uri.getHost());
+ svcInfo.put("port", ""+uri.getPort());
+ svcInfo.put("path", (uri.getPath().startsWith("/") ? uri.getPath().substring(1): uri.getPath()));
+ svcInfo.put("vhost", svcInfo.get("path"));
+ } catch (URISyntaxException e) {}
+ }
+ return svcInfo;
+ }
+
+ private static String testUriSchemes(CloudFoundryClient client, String[] uriSchemes, String uriAuthority) {
+ int i = 0;
+ String scheme = null;
+ while (i < uriSchemes.length) {
+ scheme = uriSchemes[i];
+ String uriToUse = scheme + "//" + uriAuthority;
+ try {
+ getTunnelProtocolVersion(client, uriToUse);
+ break;
+ } catch (ResourceAccessException e) {
+ if (e.getMessage().contains("refused")) {
+ i++;
+ }
+ else {
+ throw e;
+ }
+ } catch (RuntimeException e) {
+ throw e;
+ }
+ }
+ return scheme;
+ }
+
+ public static String getTunnelProtocolVersion(CloudFoundryClient client, String uri) {
+ String uriToUse = uri + "/info";
+ HttpHeaders requestHeaders = new HttpHeaders();
+ requestHeaders.set("Auth-Token", getTunnelAuth(client));
+ HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
+ HttpEntity<String> response = restTemplate.exchange(uriToUse, HttpMethod.GET, requestEntity, String.class);
+ return response.getBody().trim();
+ }
+
+ public static Map<String, String> convertJsonToMap(String json) throws IOException {
+ Map<String, String> svcInfo =
+ objectMapper.readValue(json, TypeFactory.mapType(HashMap.class, String.class, String.class));
+ return svcInfo;
+ }
+
+}
View
120 cloudfoundry-caldecott-lib/src/main/java/org/cloudfoundry/caldecott/client/TunnelServer.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.cloudfoundry.caldecott.TunnelException;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.scheduling.concurrent.ExecutorConfigurationSupport;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+
+/**
+ * A class responsible for starting and stopping the TunnelAcceptor based on the configuration options.
+ *
+ * @author Thomas Risberg
+ */
+public class TunnelServer {
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ // configuration options
+ private final InetSocketAddress local;
+ private final TunnelFactory tunnelFactory;
+ private final TaskExecutor taskExecutor;
+
+ // variables to keep state for server
+ private final ServerSocket serverSocket;
+ private TunnelAcceptor acceptor;
+
+ public TunnelServer(InetSocketAddress local, TunnelFactory tunnelFactory) {
+ this(local, tunnelFactory, getDefaultThreadExecutor());
+ }
+
+ public TunnelServer(InetSocketAddress local, TunnelFactory tunnelFactory, TaskExecutor taskExecutor) {
+ this.local = local;
+ this.tunnelFactory = tunnelFactory;
+ this.taskExecutor = taskExecutor;
+ try {
+ this.serverSocket = new ServerSocket();
+ serverSocket.setReuseAddress(true);
+ serverSocket.bind(local);
+ } catch (IOException e) {
+ throw new TunnelException("Error configuring server socket", e);
+ }
+ }
+
+ public void start() {
+ logger.info("Starting server on " + local);
+ initializeTaskExecutor(taskExecutor);
+ synchronized (this) {
+ if (acceptor == null) {
+ this.acceptor = new TunnelAcceptor(serverSocket, tunnelFactory, taskExecutor);
+ acceptor.start();
+ }
+ else {
+ throw new TunnelException("Server already running.");
+ }
+ }
+ }
+
+ public void stop() {
+ logger.info("Stopping server on " + local);
+ synchronized (this) {
+ shutdownTaskExecutor(taskExecutor);
+ if (acceptor != null) {
+ acceptor.stop();
+ if (acceptor.isActive()) {
+ logger.info("Server is actively servicing connections, waiting for client to close");
+ while (acceptor.isActive()) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException ignore) {}
+ }
+ logger.info("Server on " + local + " is now stopped");
+ }
+ }
+ else {
+ throw new TunnelException("Server is not running.");
+ }
+ }
+ }
+
+ protected static void initializeTaskExecutor(TaskExecutor taskExecutor) {
+ if (taskExecutor instanceof ExecutorConfigurationSupport) {
+ ((ExecutorConfigurationSupport)taskExecutor).initialize();
+ }
+ }
+
+ protected static void shutdownTaskExecutor(TaskExecutor taskExecutor) {
+ if (taskExecutor instanceof ExecutorConfigurationSupport) {
+ ((ExecutorConfigurationSupport)taskExecutor).shutdown();
+ }
+ }
+
+ protected static TaskExecutor getDefaultThreadExecutor() {
+ ThreadPoolTaskExecutor te = new ThreadPoolTaskExecutor();
+ te.setCorePoolSize(5);
+ te.setMaxPoolSize(10);
+ te.setQueueCapacity(100);
+ return te;
+ }
+}
View
35 cloudfoundry-caldecott-lib/src/main/resources/log4j.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
+
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
+
+ <!-- Appenders -->
+ <appender name="log" class="org.apache.log4j.RollingFileAppender">
+ <param name="file" value="caldecott.log" />
+ <param name="MaxFileSize" value="100KB"/>
+ <param name="MaxBackupIndex" value="1"/>
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="%-5p: %C{1} [%t] - %m%n" />
+ </layout>
+ </appender>
+
+ <logger name="org.cloudfoundry.caldecott">
+ <level value="info" />
+ </logger>
+ <logger name="org.cloudfoundry.caldecott.client">
+ <level value="debug" />
+ </logger>
+ <logger name="org.cloudfoundry.caldecott.client.HttpTunnel">
+ <level value="debug" />
+ </logger>
+ <logger name="org.springframework.web.client.RestTemplate">
+ <level value="error" />
+ </logger>
+
+ <!-- Root Logger -->
+ <root>
+ <priority value="warn" />
+ <appender-ref ref="log" />
+ </root>
+
+</log4j:configuration>
View
139 cloudfoundry-caldecott-lib/src/test/java/org/cloudfoundry/caldecott/JavaTunnel.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott;
+
+import org.cloudfoundry.caldecott.client.HttpTunnelFactory;
+import org.cloudfoundry.caldecott.client.TunnelHelper;
+import org.cloudfoundry.caldecott.client.TunnelServer;
+import org.cloudfoundry.client.lib.CloudFoundryClient;
+import org.cloudfoundry.client.lib.CloudService;
+
+import java.io.Console;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A class used for testing tunnels. Starts a tunnel server based on provided connection parameters.
+ *
+ * @author Thomas Risberg
+ */
+public class JavaTunnel {
+
+ private static final String CC_URL = System.getProperty("vcap.target", "https://api.cloudfoundry.com");
+ private static String vcap_email = System.getProperty("vcap.email");
+ private static String vcap_passwd = System.getProperty("vcap.passwd");
+ private static String vcap_service = System.getProperty("vcap.service");
+ public static final int LOCAL_PORT = 10000;
+ public static final String LOCAL_HOST = "localhost";
+
+ public static void main(String[] args) {
+
+ Console console = System.console();
+ //read user name, using java.util.Formatter syntax :
+ if (vcap_email == null) {
+ vcap_email = console.readLine("Login E-Mail? ");
+ }
+ //read the password, without echoing the output
+ if (vcap_passwd == null) {
+ vcap_passwd = new String(console.readPassword("Password? "));
+ }
+
+ CloudFoundryClient client = clientInit();
+
+ while (vcap_service == null) {
+ System.out.println("You have the following services defined:");
+ List<CloudService> services = client.getServices();
+ int i = 0;
+ for (CloudService svc : services) {
+ i++;
+ System.out.println(i + ": " + svc.getName());
+ }
+ if (i ==0) {
+ System.err.println("It looks like you don't have any services defined. Please create one first!");
+ System.exit(1);
+ }
+ String svc = console.readLine("Which Service to connect to (" + 1 + "-" + i +")? ");
+ int svc_ix = 0;
+ try {
+ svc_ix = Integer.parseInt(svc);
+ } catch (NumberFormatException e) {
+ System.err.println(svc + " is not a valid choice!");
+ continue;
+ }
+ if (svc_ix < 1 || svc_ix > i) {
+ System.err.println(svc + " is not a valid choice!");
+ continue;
+ }
+ vcap_service = services.get(svc_ix - 1).getName();
+ }
+ System.out.println("Starting tunnel on " + CC_URL + " to service " + vcap_service + " on behalf of " + vcap_email);
+
+ TunnelHelper.bindServiceToTunnelApp(client, vcap_service);
+
+ InetSocketAddress local = new InetSocketAddress(LOCAL_HOST, LOCAL_PORT);
+ String url = TunnelHelper.getTunnelUri(client);
+ Map<String, String> info = TunnelHelper.getTunnelServiceInfo(client, vcap_service);
+ String host = info.get("hostname");
+ int port = Integer.valueOf(info.get("port"));
+ String auth = TunnelHelper.getTunnelAuth(client);
+
+ String svc_username = info.get("username");
+ String svc_passwd = info.get("password");
+ String svc_dbname = info.get("db") != null ? info.get("db") : info.get("name");
+ String txt_dbname = info.get("db") != null ? "db" : "name";
+ String svc_vhost = info.get("vhost");
+
+ TunnelServer server = new TunnelServer(local, new HttpTunnelFactory(url, host, port, auth));
+
+ server.start();
+
+ System.out.println("Tunnel is running on " + LOCAL_HOST +" port " + LOCAL_PORT + " with auth=" + auth);
+ if (svc_vhost != null) {
+ System.out.println("Connect client with username=" + svc_username +" password=" + svc_passwd + " " + "vhost=" + svc_vhost);
+ }
+ else {
+ System.out.println("Connect client with username=" + svc_username +" password=" + svc_passwd + " " + txt_dbname + "=" + svc_dbname);
+ }
+ while (true) {
+ String command = console.readLine("Enter exit to stop: ");
+ if (command.toLowerCase().equals("exit")) {
+ break;
+ }
+ }
+ server.stop();
+
+ finalize(client);
+ System.out.println("DONE!");
+ }
+
+ public static CloudFoundryClient clientInit() {
+ CloudFoundryClient client = null;
+ try {
+ client = new CloudFoundryClient(vcap_email, vcap_passwd, CC_URL);
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ client.login();
+ return client;
+ }
+
+ public static void finalize(CloudFoundryClient client) {
+ client.logout();
+ }
+}
View
114 cloudfoundry-caldecott-lib/src/test/java/org/cloudfoundry/caldecott/client/HttpTunnelTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RequestCallback;
+import org.springframework.web.client.ResponseExtractor;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Mock tests for the HttpTunnel
+ *
+ * @author Thomas Risberg
+ */
+public class HttpTunnelTest {
+
+ HttpTunnel httpTunnel;
+
+ HttpTunnelFactory httpTunnelFactory;
+
+ @Mock
+ RestTemplate restTemplate;
+
+ @Mock
+ ResponseEntity<String> response;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ httpTunnelFactory = new HttpTunnelFactory("http://api.vcap.me", "localhost", 10000, "test", restTemplate);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testOpenTunnel() {
+ openTunnel();
+ verify(restTemplate).postForObject(isA(String.class), isA(HttpEntity.class), isA(Class.class));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testClosingTunnel() {
+ openTunnel();
+ httpTunnel.close();
+ verify(restTemplate).exchange(isA(String.class), isA(HttpMethod.class), isA(HttpEntity.class), (Class)isNull());
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testWritingSomeBytes() {
+ final byte[] data = "This is some data to write".getBytes();
+ openTunnel();
+ final byte[][] sent = new byte[1][1];
+ when(restTemplate.exchange(isA(String.class), isA(HttpMethod.class), isA(HttpEntity.class), (Class)isNull()))
+ .thenAnswer(new Answer<ResponseEntity<?>>() {
+ public ResponseEntity<?> answer(InvocationOnMock invocation) throws Throwable {
+ Object[] args = invocation.getArguments();
+ HttpEntity arg3 = (HttpEntity) args[2];
+ sent[0] = (byte[]) arg3.getBody();
+ return response;
+ }
+ });
+ httpTunnel.write(data);
+ verify(restTemplate).exchange(isA(String.class), isA(HttpMethod.class), isA(HttpEntity.class), (Class)isNull());
+ assertEquals(new String(data), new String(sent[0]));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testReadingSomeBytes() {
+ final byte[] data = "This is some data to read".getBytes();
+ openTunnel();
+ when(restTemplate.execute(isA(String.class), isA(HttpMethod.class), isA(RequestCallback.class),
+ isA(ResponseExtractor.class))).thenReturn(data);
+ final byte[] answer = httpTunnel.read(false);
+ verify(restTemplate).execute(isA(String.class), isA(HttpMethod.class), isA(RequestCallback.class),
+ isA(ResponseExtractor.class));
+ assertEquals(new String(data), new String(answer));
+ }
+
+ @SuppressWarnings("unchecked")
+ private void openTunnel() {
+ when(restTemplate.postForObject(isA(String.class), isA(HttpEntity.class), isA(Class.class))).thenReturn("{}");
+ httpTunnel = (HttpTunnel) httpTunnelFactory.createTunnel();
+ }
+}
View
97 cloudfoundry-caldecott-lib/src/test/java/org/cloudfoundry/caldecott/client/SocketClientTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2009-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cloudfoundry.caldecott.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+/**
+ * Mock tests for the SocketClient
+ *
+ * @author Thomas Risberg
+ */
+public class SocketClientTest {
+
+ @Mock
+ Socket socket;
+
+ @Mock
+ InputStream inputStream;
+
+ @Mock
+ OutputStream outputStream;
+
+ SocketClient client;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ client = new SocketClient(socket);
+ }
+
+ @Test
+ public void testReadSomeBytes() throws IOException {
+ final byte[] data = "This is some data to read".getBytes();
+ when(socket.getInputStream()).thenReturn(inputStream);
+ when(inputStream.read(isA(byte[].class))).thenAnswer( new Answer<Integer>() {
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ Object[] args = invocation.getArguments();
+ byte[] arg1 = (byte[]) args[0];
+ for (int i = 0; i < data.length; i++) {
+ arg1[i] = data[i];
+ }
+ return data.length;
+ }
+ });
+ byte[] answer = client.read();
+ assertEquals(new String(data), new String(answer));
+ assertTrue(client.isActive());
+ when(inputStream.read(isA(byte[].class))).thenReturn(-1);
+ client.read();
+ assertFalse(client.isActive());
+ }
+
+ @Test
+ public void testWriteSomeBytes() throws IOException {
+ final byte[] data = "This is some data to write".getBytes();
+ final byte[] result;
+ when(socket.getOutputStream()).thenReturn(outputStream);
+ ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
+ client.write(data);
+ verify(outputStream).write(captor.capture());
+ assertEquals(new String(data), new String(captor.getValue()));
+ verify(outputStream).flush();
+ }
+
+}
View
1  cloudfoundry-caldecott-lib/tunnel.sh
@@ -0,0 +1 @@
+mvn --quiet exec:java -Dexec.mainClass="org.cloudfoundry.caldecott.JavaTunnel" -Dexec.classpathScope="test" $*
View
2  cloudfoundry-caldecott-test/.gitignore
@@ -0,0 +1,2 @@
+/*.iml
+/target/
View
1,602 cloudfoundry-caldecott-test/data/load.json
@@ -0,0 +1,1602 @@
+{ "records": [
+ { "record":
+ { "_id" : 1,
+ "name" : "Lucas",
+ "address" : "9665 Commodo Av.",
+ "city" : "Truth or Consequences",
+ "country" : "Bosnia and Herzegovina",
+ "age" : 11}
+ },
+ { "record":
+ { "_id" : 2,
+ "name" : "Shelley",
+ "address" : "P.O. Box 635, 3581 Nunc Street",
+ "city" : "Enfield",
+ "country" : "Turkmenistan",
+ "age" : 69}
+ },
+ { "record":
+ { "_id" : 3,
+ "name" : "Skyler",
+ "address" : "Ap #645-2695 Nulla Ave",
+ "city" : "San Gabriel",
+ "country" : "Tuvalu",
+ "age" : 56}
+ },
+ { "record":
+ { "_id" : 4,
+ "name" : "Portia",
+ "address" : "1179 Sed St.",
+ "city" : "Santa Clarita",
+ "country" : "Japan",
+ "age" : 96}
+ },
+ { "record":
+ { "_id" : 5,
+ "name" : "Palmer",
+ "address" : "5214 Massa. Av.",
+ "city" : "Port Arthur",
+ "country" : "Uzbekistan",
+ "age" : 85}
+ },
+ { "record":
+ { "_id" : 6,
+ "name" : "Taylor",
+ "address" : "P.O. Box 515, 8741 Sed St.",
+ "city" : "Asbury Park",
+ "country" : "Suriname",
+ "age" : 62}
+ },
+ { "record":
+ { "_id" : 7,
+ "name" : "Nelle",
+ "address" : "429-4356 Sit Av.",
+ "city" : "Durant",
+ "country" : "Turks and Caicos Islands",
+ "age" : 63}
+ },
+ { "record":
+ { "_id" : 8,
+ "name" : "Brennan",
+ "address" : "Ap #984-1800 Convallis Road",
+ "city" : "Williston",
+ "country" : "Suriname",
+ "age" : 3}
+ },
+ { "record":
+ { "_id" : 9,
+ "name" : "Jordan",
+ "address" : "510-5341 Eget Avenue",
+ "city" : "Manassas Park",
+ "country" : "New Caledonia",
+ "age" : 100}
+ },
+ { "record":
+ { "_id" : 10,
+ "name" : "Henry",
+ "address" : "P.O. Box 425, 2861 Lorem Av.",
+ "city" : "Toledo",
+ "country" : "Sweden",
+ "age" : 65}
+ },
+ { "record":
+ { "_id" : 11,
+ "name" : "Florence",
+ "address" : "2300 Nec Rd.",
+ "city" : "Morrison",
+ "country" : "Heard Island and Mcdonald Islands",
+ "age" : 79}
+ },
+ { "record":
+ { "_id" : 12,
+ "name" : "Lane",
+ "address" : "622-2648 Augue Ave",
+ "city" : "White Plains",
+ "country" : "Mauritius",
+ "age" : 60}
+ },
+ { "record":
+ { "_id" : 13,
+ "name" : "Illana",
+ "address" : "P.O. Box 172, 5207 In St.",
+ "city" : "Portland",
+ "country" : "Turkmenistan",
+ "age" : 54}
+ },
+ { "record":
+ { "_id" : 14,
+ "name" : "Autumn",
+ "address" : "8389 Augue Road",
+ "city" : "Riverside",
+ "country" : "Antarctica",
+ "age" : 40}
+ },
+ { "record":
+ { "_id" : 15,
+ "name" : "Nicole",
+ "address" : "P.O. Box 138, 5642 Sed Avenue",
+ "city" : "Norwich",
+ "country" : "Slovenia",
+ "age" : 98}
+ },
+ { "record":
+ { "_id" : 16,
+ "name" : "Cyrus",
+ "address" : "6595 Et Avenue",
+ "city" : "Harrisburg",
+ "country" : "Thailand",
+ "age" : 78}
+ },
+ { "record":
+ { "_id" : 17,
+ "name" : "Igor",
+ "address" : "P.O. Box 884, 8228 Integer Street",
+ "city" : "Davis",
+ "country" : "Azerbaijan",
+ "age" : 25}
+ },
+ { "record":
+ { "_id" : 18,
+ "name" : "Cameron",
+ "address" : "P.O. Box 370, 7095 Sit St.",
+ "city" : "Kankakee",
+ "country" : "Fiji",
+ "age" : 80}
+ },
+ { "record":
+ { "_id" : 19,
+ "name" : "Jin",
+ "address" : "840-6770 Et Rd.",
+ "city" : "Guánica",
+ "country" : "Samoa",
+ "age" : 47}
+ },
+ { "record":
+ { "_id" : 20,
+ "name" : "Adrienne",
+ "address" : "5612 Dictum Road",
+ "city" : "Florence",
+ "country" : "Bangladesh",
+ "age" : 70}
+ },
+ { "record":
+ { "_id" : 21,
+ "name" : "Cassady",
+ "address" : "P.O. Box 230, 824 Morbi Avenue",
+ "city" : "Cape Coral",
+ "country" : "Burundi",
+ "age" : 26}
+ },
+ { "record":
+ { "_id" : 22,
+ "name" : "Inez",
+ "address" : "Ap #684-1929 Turpis Ave",
+ "city" : "Mission Viejo",
+ "country" : "Benin",
+ "age" : 48}
+ },
+ { "record":
+ { "_id" : 23,
+ "name" : "Hector",
+ "address" : "P.O. Box 307, 8478 Tristique Av.",
+ "city" : "Farrell",
+ "country" : "Mexico",
+ "age" : 64}
+ },
+ { "record":
+ { "_id" : 24,
+ "name" : "Sharon",
+ "address" : "Ap #133-6077 Duis Ave",
+ "city" : "La Verne",
+ "country" : "Eritrea",
+ "age" : 75}
+ },
+ { "record":
+ { "_id" : 25,
+ "name" : "Kenneth",
+ "address" : "8388 Egestas, Avenue",
+ "city" : "Revere",
+ "country" : "Libyan Arab Jamahiriya",
+ "age" : 56}
+ },
+ { "record":
+ { "_id" : 26,
+ "name" : "Hedy",
+ "address" : "244-5276 Magna St.",
+ "city" : "Edmond",
+ "country" : "Zambia",
+ "age" : 69}
+ },
+ { "record":
+ { "_id" : 27,
+ "name" : "Jolie",
+ "address" : "P.O. Box 525, 8242 Augue, Avenue",
+ "city" : "Vincennes",
+ "country" : "Chile",
+ "age" : 37}
+ },
+ { "record":
+ { "_id" : 28,
+ "name" : "Xandra",
+ "address" : "P.O. Box 579, 3814 Consectetuer Av.",
+ "city" : "North Las Vegas",
+ "country" : "Ireland",
+ "age" : 18}
+ },
+ { "record":
+ { "_id" : 29,
+ "name" : "Jermaine",
+ "address" : "P.O. Box 612, 3644 Eu, Ave",
+ "city" : "Christiansted",
+ "country" : "Azerbaijan",
+ "age" : 52}
+ },
+ { "record":
+ { "_id" : 30,
+ "name" : "Naomi",
+ "address" : "830-3601 Aenean Rd.",
+ "city" : "Grambling",
+ "country" : "Tajikistan",
+ "age" : 83}
+ },
+ { "record":
+ { "_id" : 31,
+ "name" : "Carter",
+ "address" : "Ap #791-7937 Integer St.",
+ "city" : "Hastings",
+ "country" : "Albania",
+ "age" : 33}
+ },
+ { "record":
+ { "_id" : 32,
+ "name" : "Pearl",
+ "address" : "1317 Et Ave",
+ "city" : "Vermillion",
+ "country" : "Morocco",
+ "age" : 17}
+ },
+ { "record":
+ { "_id" : 33,
+ "name" : "Joseph",
+ "address" : "Ap #524-270 Nunc, Street",
+ "city" : "Tacoma",
+ "country" : "Western Sahara",
+ "age" : 39}
+ },
+ { "record":
+ { "_id" : 34,
+ "name" : "Naida",
+ "address" : "Ap #249-9011 Ligula Ave",
+ "city" : "Hagerstown",
+ "country" : "Central African Republic",
+ "age" : 44}
+ },
+ { "record":
+ { "_id" : 35,
+ "name" : "Brock",
+ "address" : "P.O. Box 360, 5609 Arcu. Av.",
+ "city" : "Salinas",
+ "country" : "Argentina",
+ "age" : 48}
+ },
+ { "record":
+ { "_id" : 36,
+ "name" : "Teegan",
+ "address" : "P.O. Box 514, 365 Et St.",
+ "city" : "Hollister",
+ "country" : "American Samoa",
+ "age" : 50}
+ },
+ { "record":
+ { "_id" : 37,
+ "name" : "Hollee",
+ "address" : "5866 Commodo Rd.",
+ "city" : "Sedalia",
+ "country" : "Malawi",
+ "age" : 15}
+ },
+ { "record":
+ { "_id" : 38,
+ "name" : "Zane",
+ "address" : "P.O. Box 879, 6396 Neque Rd.",
+ "city" : "Cairo",
+ "country" : "Palau",
+ "age" : 78}
+ },
+ { "record":
+ { "_id" : 39,
+ "name" : "Hunter",
+ "address" : "738-4604 Cubilia Rd.",
+ "city" : "Jackson",
+ "country" : "Senegal",
+ "age" : 52}
+ },
+ { "record":
+ { "_id" : 40,
+ "name" : "Sydnee",
+ "address" : "3160 Fringilla Avenue",
+ "city" : "Fullerton",
+ "country" : "Seychelles",
+ "age" : 80}
+ },
+ { "record":
+ { "_id" : 41,
+ "name" : "Bradley",
+ "address" : "Ap #566-6094 Molestie Rd.",
+ "city" : "Bell Gardens",
+ "country" : "Angola",
+ "age" : 77}
+ },
+ { "record":
+ { "_id" : 42,
+ "name" : "Vielka",
+ "address" : "Ap #487-3151 Pede Avenue",
+ "city" : "South Bend",
+ "country" : "Wallis and Futuna",
+ "age" : 77}
+ },
+ { "record":
+ { "_id" : 43,
+ "name" : "Kirsten",
+ "address" : "960-753 Vel St.",
+ "city" : "Cerritos",
+ "country" : "Taiwan, Province of China",
+ "age" : 97}
+ },
+ { "record":
+ { "_id" : 44,
+ "name" : "Jakeem",
+ "address" : "365-1397 Eu, Ave",
+ "city" : "Stockton",
+ "country" : "United Kingdom",
+ "age" : 65}
+ },
+ { "record":
+ { "_id" : 45,
+ "name" : "Jena",
+ "address" : "P.O. Box 952, 3389 Nec Ave",
+ "city" : "Cape Girardeau",
+ "country" : "Mauritius",
+ "age" : 77}
+ },
+ { "record":
+ { "_id" : 46,
+ "name" : "Isaac",
+ "address" : "P.O. Box 965, 1909 Lectus, Ave",
+ "city" : "Hoboken",
+ "country" : "France",
+ "age" : 14}
+ },
+ { "record":
+ { "_id" : 47,
+ "name" : "Jael",
+ "address" : "914-1937 Faucibus St.",
+ "city" : "Clarksville",
+ "country" : "Cameroon",
+ "age" : 49}
+ },
+ { "record":
+ { "_id" : 48,
+ "name" : "Evan",
+ "address" : "5026 Magna. Road",
+ "city" : "Jeffersontown",
+ "country" : "New Caledonia",
+ "age" : 74}
+ },
+ { "record":
+ { "_id" : 49,
+ "name" : "Halla",
+ "address" : "7275 Pharetra Av.",
+ "city" : "Fremont",
+ "country" : "Greece",
+ "age" : 48}
+ },
+ { "record":
+ { "_id" : 50,
+ "name" : "Kasimir",
+ "address" : "6078 Nec Rd.",
+ "city" : "San Gabriel",
+ "country" : "Guinea",
+ "age" : 6}
+ },
+ { "record":
+ { "_id" : 51,
+ "name" : "Regina",
+ "address" : "9629 Elit. Avenue",
+ "city" : "Brookings",
+ "country" : "Kiribati",
+ "age" : 28}
+ },
+ { "record":
+ { "_id" : 52,
+ "name" : "Burke",
+ "address" : "Ap #377-8583 Vulputate, Street",
+ "city" : "Haverhill",
+ "country" : "Panama",
+ "age" : 82}
+ },
+ { "record":
+ { "_id" : 53,
+ "name" : "Daryl",
+ "address" : "Ap #967-235 Eget Rd.",
+ "city" : "Bentonville",
+ "country" : "Eritrea",
+ "age" : 67}
+ },
+ { "record":
+ { "_id" : 54,
+ "name" : "Zane",
+ "address" : "931-2607 Aliquam Rd.",
+ "city" : "Carson City",
+ "country" : "Thailand",
+ "age" : 24}
+ },
+ { "record":
+ { "_id" : 55,
+ "name" : "Bertha",
+ "address" : "219-5800 Enim Ave",
+ "city" : "Nacogdoches",
+ "country" : "San Marino",
+ "age" : 89}
+ },
+ { "record":
+ { "_id" : 56,
+ "name" : "Jaime",
+ "address" : "450-1962 A, Rd.",
+ "city" : "Scottsdale",
+ "country" : "United States Minor Outlying Islands",
+ "age" : 16}
+ },
+ { "record":
+ { "_id" : 57,
+ "name" : "Sarah",
+ "address" : "114-2987 Quisque Road",
+ "city" : "West Memphis",
+ "country" : "Guinea-bissau",
+ "age" : 22}
+ },
+ { "record":
+ { "_id" : 58,
+ "name" : "Coby",
+ "address" : "649-1000 Donec St.",
+ "city" : "Cary",
+ "country" : "Western Sahara",
+ "age" : 23}
+ },
+ { "record":
+ { "_id" : 59,
+ "name" : "Nora",
+ "address" : "Ap #136-1447 Cursus Avenue",
+ "city" : "Chula Vista",
+ "country" : "Gabon",
+ "age" : 53}
+ },
+ { "record":
+ { "_id" : 60,
+ "name" : "Ginger",
+ "address" : "Ap #970-5061 Aenean Rd.",
+ "city" : "Victoria",
+ "country" : "Korea, Republic of",
+ "age" : 52}
+ },
+ { "record":
+ { "_id" : 61,
+ "name" : "Conan",
+ "address" : "671-4014 Est. Av.",
+ "city" : "Manitowoc",
+ "country" : "Barbados",
+ "age" : 1}
+ },
+ { "record":
+ { "_id" : 62,
+ "name" : "Madaline",
+ "address" : "9374 Suscipit Avenue",
+ "city" : "Modesto",
+ "country" : "Sudan",
+ "age" : 54}
+ },
+ { "record":
+ { "_id" : 63,
+ "name" : "Lydia",
+ "address" : "5044 Aliquet Ave",
+ "city" : "Batavia",
+ "country" : "Bahamas",
+ "age" : 98}
+ },
+ { "record":
+ { "_id" : 64,
+ "name" : "Lael",
+ "address" : "548-9912 Tincidunt Rd.",
+ "city" : "Port Huron",
+ "country" : "Tuvalu",
+ "age" : 88}
+ },
+ { "record":
+ { "_id" : 65,
+ "name" : "Calista",
+ "address" : "Ap #673-2865 Malesuada Street",
+ "city" : "Fontana",
+ "country" : "Switzerland",
+ "age" : 27}
+ },
+ { "record":
+ { "_id" : 66,
+ "name" : "Cairo",
+ "address" : "3385 Erat Av.",
+ "city" : "Knoxville",
+ "country" : "Vanuatu",
+ "age" : 4}
+ },
+ { "record":
+ { "_id" : 67,
+ "name" : "Harrison",
+ "address" : "487-6677 Purus. St.",
+ "city" : "Lansing",
+ "country" : "Tunisia",
+ "age" : 16}
+ },
+ { "record":
+ { "_id" : 68,
+ "name" : "Yoko",
+ "address" : "7758 Morbi Av.",
+ "city" : "Anaheim",
+ "country" : "Kiribati",
+ "age" : 5}
+ },
+ { "record":
+ { "_id" : 69,
+ "name" : "Nero",
+ "address" : "9562 Risus. Street",
+ "city" : "Murray",
+ "country" : "Burundi",
+ "age" : 76}
+ },
+ { "record":
+ { "_id" : 70,
+ "name" : "Ira",
+ "address" : "952 Non St.",
+ "city" : "Buena Park",
+ "country" : "Ethiopia",
+ "age" : 55}
+ },
+ { "record":
+ { "_id" : 71,
+ "name" : "Colleen",
+ "address" : "2167 Orci Avenue",
+ "city" : "Tallahassee",
+ "country" : "Argentina",
+ "age" : 19}
+ },
+ { "record":
+ { "_id" : 72,
+ "name" : "Rachel",
+ "address" : "Ap #940-5542 Et St.",
+ "city" : "Murfreesboro",
+ "country" : "New Zealand",
+ "age" : 65}
+ },
+ { "record":
+ { "_id" : 73,
+ "name" : "Uma",
+ "address" : "Ap #800-3604 Sapien. Rd.",
+ "city" : "West Hartford",
+ "country" : "Tunisia",
+ "age" : 44}
+ },
+ { "record":
+ { "_id" : 74,
+ "name" : "Shana",
+ "address" : "P.O. Box 582, 7739 Eget Rd.",
+ "city" : "Springfield",
+ "country" : "Luxembourg",
+ "age" : 66}
+ },
+ { "record":
+ { "_id" : 75,
+ "name" : "Ariana",
+ "address" : "246-6080 Nullam Ave",
+ "city" : "Gold Beach",
+ "country" : "Poland",
+ "age" : 54}
+ },
+ { "record":
+ { "_id" : 76,
+ "name" : "Tanek",
+ "address" : "Ap #303-8079 Nunc St.",
+ "city" : "Casper",
+ "country" : "Saint Helena",
+ "age" : 59}
+ },
+ { "record":
+ { "_id" : 77,
+ "name" : "Joseph",
+ "address" : "5473 Rhoncus. Rd.",
+ "city" : "Las Cruces",
+ "country" : "Lesotho",
+ "age" : 67}
+ },
+ { "record":
+ { "_id" : 78,
+ "name" : "Akeem",
+ "address" : "Ap #920-5907 Aptent Rd.",
+ "city" : "Valdosta",
+ "country" : "Myanmar",
+ "age" : 84}
+ },
+ { "record":
+ { "_id" : 79,
+ "name" : "Ava",
+ "address" : "7480 In, St.",
+ "city" : "Bell",
+ "country" : "Bermuda",
+ "age" : 92}
+ },
+ { "record":
+ { "_id" : 80,
+ "name" : "Lucian",
+ "address" : "754 Cum Ave",
+ "city" : "Fountain Valley",
+ "country" : "Dominica",
+ "age" : 77}
+ },
+ { "record":
+ { "_id" : 81,
+ "name" : "Rhiannon",
+ "address" : "699-9737 Dictum Street",
+ "city" : "Fort Collins",
+ "country" : "Uganda",
+ "age" : 50}
+ },
+ { "record":
+ { "_id" : 82,
+ "name" : "Sara",
+ "address" : "Ap #497-9220 Iaculis, St.",
+ "city" : "Sheridan",
+ "country" : "Niue",
+ "age" : 11}
+ },
+ { "record":
+ { "_id" : 83,
+ "name" : "Madeson",
+ "address" : "Ap #380-7810 Turpis. St.",
+ "city" : "Augusta",
+ "country" : "Antigua and Barbuda",
+ "age" : 53}
+ },
+ { "record":
+ { "_id" : 84,
+ "name" : "Bo",
+ "address" : "1520 Lacinia Ave",
+ "city" : "West Hartford",
+ "country" : "Chad",
+ "age" : 43}
+ },
+ { "record":
+ { "_id" : 85,
+ "name" : "Macey",
+ "address" : "Ap #683-5646 Vitae Ave",
+ "city" : "Hialeah",
+ "country" : "Romania",
+ "age" : 32}
+ },
+ { "record":
+ { "_id" : 86,
+ "name" : "Sacha",
+ "address" : "777-7670 Tristique St.",
+ "city" : "Stillwater",
+ "country" : "Tuvalu",
+ "age" : 49}
+ },
+ { "record":
+ { "_id" : 87,
+ "name" : "Chancellor",
+ "address" : "P.O. Box 308, 6468 Nibh Rd.",
+ "city" : "Miami Beach",
+ "country" : "Albania",
+ "age" : 52}
+ },
+ { "record":
+ { "_id" : 88,
+ "name" : "Neville",
+ "address" : "P.O. Box 620, 8472 Sem Ave",
+ "city" : "St. Marys",
+ "country" : "Brazil",
+ "age" : 50}
+ },
+ { "record":
+ { "_id" : 89,
+ "name" : "Ian",
+ "address" : "Ap #399-8794 Amet, Street",
+ "city" : "Sturgis",
+ "country" : "Slovenia",
+ "age" : 64}
+ },
+ { "record":
+ { "_id" : 90,
+ "name" : "Malik",
+ "address" : "P.O. Box 867, 2644 Curabitur Rd.",
+ "city" : "Ocean City",
+ "country" : "Bahamas",
+ "age" : 49}
+ },
+ { "record":
+ { "_id" : 91,
+ "name" : "Elvis",
+ "address" : "3359 Malesuada Rd.",
+ "city" : "Kingsport",
+ "country" : "Lithuania",
+ "age" : 46}
+ },
+ { "record":
+ { "_id" : 92,
+ "name" : "Alice",
+ "address" : "P.O. Box 860, 7353 Lorem Street",
+ "city" : "Orange",
+