// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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 com.google.api.ads.common.lib.testing;
import static org.apache.commons.lang.CharEncoding.UTF_8;
import com.google.common.collect.Lists;
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;
import org.mortbay.http.HttpContext;
import org.mortbay.http.HttpException;
import org.mortbay.http.HttpFields;
import org.mortbay.http.HttpMessage;
import org.mortbay.http.HttpRequest;
import org.mortbay.http.HttpResponse;
import org.mortbay.jetty.Server;
import org.mortbay.util.InetAddrPort;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Deque;
import java.util.List;
import java.util.zip.GZIPInputStream;
/**
* HTTP server used to verify requests and send mocked responses.
*/
public class TestHttpServer {
private InternalHttpServer server;
/**
* Default constructor.
*/
public TestHttpServer() {}
/**
* Starts the HTTP server.
*
* @throws Exception an arbitrary exception may be thrown
*/
public void start() throws Exception {
server = new InternalHttpServer(TestPortFinder.getInstance().checkOutUnusedPort());
server.start();
}
/**
* Stops the HTTP server.
*
* @throws InterruptedException Stopping a lifecycle is rarely atomic and may be interrupted by
* another thread. If this happens InterruptedException is throw and the component will be
* in an indeterminant state and should probably be discarded.
*/
public void stop() throws InterruptedException {
server.stop();
TestPortFinder.getInstance().releaseUnusedPort(server.port);
}
/**
* Gets the body of the last request made to the server. This will be the inflated body if
* the last request was compressed.
*/
public String getLastRequestBody() {
return server.getLastRequestBody();
}
/**
* Gets if the body of the last request made to the server was compressed.
*/
public boolean wasLastRequestBodyCompressed() {
return server.wasLastRequestBodyCompressed();
}
/**
* Gets the body of the all requests made to the server, in order from oldest
* to newest.
*/
public List<String> getAllRequestBodies() {
return Lists.newArrayList(server.requestBodies);
}
/**
* Gets the authorization header of the last request made to the server or
* {@code null} if none.
*/
public String getLastAuthorizationHttpHeader() {
return server.getLastAuthorizationHttpHeader();
}
/**
* Gets the authorization headers of the all request made to the server, in
* order from oldest to newest. If a request did not contain an authorization
* header, its index contains {@code null}.
*/
public List<String> getAllAuthorizationHttpHeaders() {
return Lists.newArrayList(server.authorizationHttpHeaders);
}
/**
* Sets the response body to return on the next request.
*/
public void setMockResponseBody(String mockResponseBody) {
setMockResponseBodies(Lists.newArrayList(mockResponseBody));
}
/**
* Sets the response bodies to return on subsequent requests.
*/
public void setMockResponseBodies(List<String> mockResponseBodies) {
server.mockResponseBodies.clear();
server.mockResponseBodies.addAll(mockResponseBodies);
}
/**
* Sets the delay in milliseconds before the server responds.
*/
public void setDelay(long delay) {
server.delay = delay;
}
/**
* Gets the server URL with port.
*/
public String getServerUrl() {
return server.getServerUrl();
}
/**
* Jetty5 implementation of an HTTP server.
*/
private class InternalHttpServer extends Server {
private final int port;
private int numInteractions = 0;
private long delay = 0;
private final Deque<String> requestBodies = Lists.newLinkedList();
private final Deque<Boolean> requestBodiesCompressionStates = Lists.newLinkedList();
private final Deque<String> authorizationHttpHeaders = Lists.newLinkedList();
private final List<String> mockResponseBodies = Lists.newArrayList();
/**
* Default constructor.
*
* @throws IOException if port could not be set
*/
public InternalHttpServer(int port) throws IOException {
super();
this.port = port;
addListener(new InetAddrPort(port));
}
/**
* Gets the server URL with port.
*/
public String getServerUrl() {
return String.format("http://localhost:%s", port);
}
@Override
public HttpContext service(final HttpRequest request, final HttpResponse response)
throws IOException, HttpException {
request.setState(HttpMessage.__MSG_EDITABLE);
this.authorizationHttpHeaders.add(request.getHeader().get("Authorization"));
// Read the raw bytes from the request.
final byte[] rawRequestBytes = new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return request.getInputStream();
}
}.read();
// Inflate the raw bytes if they are in gzip format.
boolean isGzipFormat = "gzip".equals(request.getHeader().get(HttpFields.__ContentEncoding));
byte[] requestBytes;
if (isGzipFormat) {
requestBytes = new ByteSource(){
@Override
public InputStream openStream() throws IOException {
return new GZIPInputStream(ByteSource.wrap(rawRequestBytes).openStream());
}
}.read();
} else {
requestBytes = rawRequestBytes;
}
// Convert the (possibly inflated) request bytes to a string.
this.requestBodies.add(
ByteSource.wrap(requestBytes).asCharSource(Charset.forName(UTF_8)).read());
this.requestBodiesCompressionStates.add(isGzipFormat);
// Simulate a delay in processing.
simulateDelay();
new ByteSink() {
@Override
public OutputStream openStream() {
return response.getOutputStream();
}
}.asCharSink(Charset.forName(UTF_8)).write(mockResponseBodies.get(numInteractions++));
return getContext(getServerUrl());
}
/**
* Simulates delays in processing requests.
*/
private void simulateDelay() throws HttpException {
try {
Thread.sleep(this.delay);
} catch (InterruptedException e) {
throw new HttpException(500, e.getMessage());
}
}
/**
* Gets the body of the last request made to the server.
*/
private String getLastRequestBody() {
return requestBodies.getLast();
}
/**
* Returns if the last request body was compressed.
*/
private boolean wasLastRequestBodyCompressed() {
return requestBodiesCompressionStates.getLast();
}
/**
* Gets the authorization header of the last request made to the server or
* {@code null} if none.
*/
private String getLastAuthorizationHttpHeader() {
return authorizationHttpHeaders.getLast();
}
}
}