/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.camel.itest.http;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;
import org.apache.camel.test.AvailablePortFinder;
import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.HttpResponseFactory;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpServerConnection;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.localserver.EchoHandler;
import org.apache.http.localserver.RandomHandler;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.params.SyncBasicHttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpExpectationVerifier;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.protocol.HttpRequestHandlerRegistry;
import org.apache.http.protocol.HttpService;
import org.apache.http.protocol.ImmutableHttpProcessor;
import org.apache.http.protocol.ResponseConnControl;
import org.apache.http.protocol.ResponseContent;
import org.apache.http.protocol.ResponseDate;
import org.apache.http.protocol.ResponseServer;
/**
* Copy of org.apache.http.localserver.LocalTestServer to use a specific port.
*/
public class HttpTestServer {
public static final int PORT = AvailablePortFinder.getNextAvailable(18080);
/**
* The local address to bind to.
* The host is an IP number rather than "localhost" to avoid surprises
* on hosts that map "localhost" to an IPv6 address or something else.
* The port is 0 to let the system pick one.
*/
public static final InetSocketAddress TEST_SERVER_ADDR =
new InetSocketAddress("localhost", PORT);
/** The request handler registry. */
private final HttpRequestHandlerRegistry handlerRegistry;
private final HttpService httpservice;
/** Optional SSL context */
private final SSLContext sslcontext;
/** The server socket, while being served. */
private volatile ServerSocket servicedSocket;
/** The request listening thread, while listening. */
private volatile ListenerThread listenerThread;
/** Set of active worker threads */
private final Set<Worker> workers;
/** The number of connections this accepted. */
private final AtomicInteger acceptedConnections = new AtomicInteger(0);
static {
//set them as system properties so Spring can use the property placeholder
//things to set them into the URL's in the spring contexts
System.setProperty("HttpTestServer.Port", Integer.toString(PORT));
}
/**
* Creates a new test server.
*
* @param proc the HTTP processors to be used by the server, or
* <code>null</code> to use a
* {@link #newProcessor default} processor
* @param reuseStrat the connection reuse strategy to be used by the
* server, or <code>null</code> to use
* {@link #newConnectionReuseStrategy() default}
* strategy.
* @param params the parameters to be used by the server, or
* <code>null</code> to use
* {@link #newDefaultParams default} parameters
* @param sslcontext optional SSL context if the server is to leverage
* SSL/TLS transport security
*/
public HttpTestServer(
final BasicHttpProcessor proc,
final ConnectionReuseStrategy reuseStrat,
final HttpResponseFactory responseFactory,
final HttpExpectationVerifier expectationVerifier,
final HttpParams params,
final SSLContext sslcontext) {
this.handlerRegistry = new HttpRequestHandlerRegistry();
this.workers = Collections.synchronizedSet(new HashSet<Worker>());
this.httpservice = new HttpService(
proc != null ? proc : newProcessor(),
reuseStrat != null ? reuseStrat : newConnectionReuseStrategy(),
responseFactory != null ? responseFactory : newHttpResponseFactory(),
handlerRegistry,
expectationVerifier,
params != null ? params : newDefaultParams());
this.sslcontext = sslcontext;
}
/**
* Creates a new test server with SSL/TLS encryption.
*
* @param sslcontext SSL context
*/
public HttpTestServer(final SSLContext sslcontext) {
this(null, null, null, null, null, sslcontext);
}
/**
* Creates a new test server.
*
* @param proc the HTTP processors to be used by the server, or
* <code>null</code> to use a
* {@link #newProcessor default} processor
* @param params the parameters to be used by the server, or
* <code>null</code> to use
* {@link #newDefaultParams default} parameters
*/
public HttpTestServer(
BasicHttpProcessor proc,
HttpParams params) {
this(proc, null, null, null, params, null);
}
/**
* Obtains an HTTP protocol processor with default interceptors.
*
* @return a protocol processor for server-side use
*/
protected HttpProcessor newProcessor() {
return new ImmutableHttpProcessor(new HttpResponseInterceptor[] {new ResponseDate(),
new ResponseServer(),
new ResponseContent(),
new ResponseConnControl()});
}
/**
* Obtains a set of reasonable default parameters for a server.
*
* @return default parameters
*/
protected HttpParams newDefaultParams() {
HttpParams params = new SyncBasicHttpParams();
params
.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 60000)
.setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024)
.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false)
.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true)
.setParameter(CoreProtocolPNames.ORIGIN_SERVER,
"LocalTestServer/1.1");
return params;
}
protected ConnectionReuseStrategy newConnectionReuseStrategy() {
return new DefaultConnectionReuseStrategy();
}
protected HttpResponseFactory newHttpResponseFactory() {
return new DefaultHttpResponseFactory();
}
/**
* Returns the number of connections this test server has accepted.
*/
public int getAcceptedConnectionCount() {
return acceptedConnections.get();
}
/**
* {@link #register Registers} a set of default request handlers.
* <pre>
* URI pattern Handler
* ----------- -------
* /echo/* {@link EchoHandler EchoHandler}
* /random/* {@link RandomHandler RandomHandler}
* </pre>
*/
public void registerDefaultHandlers() {
handlerRegistry.register("/echo/*", new EchoHandler());
handlerRegistry.register("/random/*", new RandomHandler());
}
/**
* Registers a handler with the local registry.
*
* @param pattern the URL pattern to match
* @param handler the handler to apply
*/
public void register(String pattern, HttpRequestHandler handler) {
handlerRegistry.register(pattern, handler);
}
/**
* Unregisters a handler from the local registry.
*
* @param pattern the URL pattern
*/
public void unregister(String pattern) {
handlerRegistry.unregister(pattern);
}
/**
* Starts this test server.
*/
public void start() throws Exception {
if (servicedSocket != null) {
throw new IllegalStateException(this.toString() + " already running");
}
ServerSocket ssock;
if (sslcontext != null) {
SSLServerSocketFactory sf = sslcontext.getServerSocketFactory();
ssock = sf.createServerSocket();
} else {
ssock = new ServerSocket();
}
ssock.setReuseAddress(true); // probably pointless for port '0'
ssock.bind(TEST_SERVER_ADDR);
servicedSocket = ssock;
listenerThread = new ListenerThread();
listenerThread.setDaemon(false);
listenerThread.start();
}
/**
* Stops this test server.
*/
public void stop() throws Exception {
if (servicedSocket == null) {
return; // not running
}
ListenerThread t = listenerThread;
if (t != null) {
t.shutdown();
}
synchronized (workers) {
for (Worker worker : workers) {
worker.shutdown();
}
}
}
public void awaitTermination(long timeMs) throws InterruptedException {
if (listenerThread != null) {
listenerThread.join(timeMs);
}
}
@Override
public String toString() {
ServerSocket ssock = servicedSocket; // avoid synchronization
StringBuilder sb = new StringBuilder(80);
sb.append("LocalTestServer/");
if (ssock == null) {
sb.append("stopped");
} else {
sb.append(ssock.getLocalSocketAddress());
}
return sb.toString();
}
/**
* Obtains the local address the server is listening on
*
* @return the service address
*/
public InetSocketAddress getServiceAddress() {
ServerSocket ssock = servicedSocket; // avoid synchronization
if (ssock == null) {
throw new IllegalStateException("not running");
}
return (InetSocketAddress) ssock.getLocalSocketAddress();
}
/**
* The request listener.
* Accepts incoming connections and launches a service thread.
*/
class ListenerThread extends Thread {
private volatile Exception exception;
@Override
public void run() {
try {
while (!interrupted()) {
Socket socket = servicedSocket.accept();
acceptedConnections.incrementAndGet();
DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
conn.bind(socket, httpservice.getParams());
// Start worker thread
Worker worker = new Worker(conn);
workers.add(worker);
worker.setDaemon(true);
worker.start();
}
} catch (Exception ex) {
this.exception = ex;
} finally {
try {
servicedSocket.close();
} catch (IOException ignore) {
}
}
}
public void shutdown() {
interrupt();
try {
servicedSocket.close();
} catch (IOException ignore) {
}
}
public Exception getException() {
return this.exception;
}
}
class Worker extends Thread {
private final HttpServerConnection conn;
private volatile Exception exception;
Worker(final HttpServerConnection conn) {
this.conn = conn;
}
@Override
public void run() {
HttpContext context = new BasicHttpContext();
try {
while (this.conn.isOpen() && !Thread.interrupted()) {
httpservice.handleRequest(this.conn, context);
}
} catch (Exception ex) {
this.exception = ex;
} finally {
workers.remove(this);
try {
this.conn.shutdown();
} catch (IOException ignore) {
}
}
}
public void shutdown() {
interrupt();
try {
this.conn.shutdown();
} catch (IOException ignore) {
}
}
public Exception getException() {
return this.exception;
}
}
}