@Override protected HttpParams createHttpParams() { HttpParams params = new SyncBasicHttpParams(); HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET); HttpConnectionParams.setTcpNoDelay(params, true); HttpConnectionParams.setSocketBufferSize(params, 8192); // determine the release version from packaged version info final VersionInfo vi = VersionInfo.loadVersionInfo("org.apache.http.client", getClass().getClassLoader()); final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE; HttpProtocolParams.setUserAgent(params, "Apache-HttpClient/" + release + " (java 1.5)"); return params; }
public class HttpServer extends Thread { /** Subclass of HttpService which adds some additional hooks to all responses */ static class MyHttpService extends HttpService { MyHttpService(HttpProcessor proc, UriHttpRequestHandlerMapper registry) { super(proc, registry); } @Override protected void doService(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { response.addHeader(HttpHeaders.CACHE_CONTROL, "must-revalidate"); // Disable caching // Normalize the URI super.doService(request, response, context); } } /** * Subclass of UriHttpRequestHandlerMapper which normalizes a URI and matches only its path. This * is because we ignore the host part (we don't use the {@code Host} header) */ static class MyRequestHandlerMapper extends UriHttpRequestHandlerMapper { @Override protected String getRequestPath(final HttpRequest request) { String s = request.getRequestLine().getUri(); try { URI uri = new URI(s); return uri.getPath(); } catch (URISyntaxException ex) { return s; } catch (IllegalArgumentException ex) { return s; } } } private volatile boolean shouldRun = true; private final DefaultBHttpServerConnectionFactory connectionFactory; private final HttpService httpService; private final UriHttpRequestHandlerMapper registry; private final Set<Worker> allWorkers = new HashSet<Worker>(); private static final String serverString = String.format( "CouchbaseMock/%s (mcd; views) httpcomponents/%s", Info.getVersion(), VersionInfo.loadVersionInfo("org.apache.http", null).getRelease()); private ServerSocketChannel listener; public static final String CX_SOCKET = "couchbase.mock.http.socket"; public static final String CX_AUTH = "couchbase.mock.http.auth"; /** * Creates a new server. To make the server respond to requests, invoke the {@link * #bind(java.net.InetSocketAddress)} method to make it use a socket, and then invoke the {@link * #start()} method to start it up in the background. * * <p>Use {@link #register(String, org.apache.http.protocol.HttpRequestHandler)} to add handlers * which respond to various URL paths */ public HttpServer() { this.connectionFactory = new DefaultBHttpServerConnectionFactory(); this.registry = new MyRequestHandlerMapper(); HttpProcessor httpProcessor = HttpProcessorBuilder.create() .add(new ResponseServer(serverString)) .add(new ResponseContent()) .add(new ResponseConnControl()) .build(); this.httpService = new MyHttpService(httpProcessor, registry); // Register the unknown handler register( "*", new HttpRequestHandler() { @Override public void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException, IOException { response.setStatusCode(HttpStatus.SC_NOT_FOUND); } }); } /** * Set the server's listening address * * @param address The address the server should listen on * @throws IOException if a new socket could not be created * @see {@link #bind(java.nio.channels.ServerSocketChannel)} */ public void bind(InetSocketAddress address) throws IOException { if (listener != null) { listener.close(); listener = null; } listener = ServerSocketChannel.open(); listener.socket().bind(address); } /** * Set the server's listening socket. * * @param newSock An existing listening socket. * @see {@link #bind(java.net.InetSocketAddress)} */ public void bind(ServerSocketChannel newSock) { listener = newSock; } /** * Register a path with a handler * * @param pattern The path to register * @param handler The handler to handle the path * @see {@link org.apache.http.protocol.UriHttpRequestHandlerMapper} */ public void register(String pattern, HttpRequestHandler handler) { registry.register(pattern, handler); registry.register(pattern + "/", handler); } /** * Unregister a given path. Further requests to paths matching the specified pattern will result * in a 404 being delivered to the client * * @param pattern The pattern to unregister. Must have previously been registered via {@link * #register(String, org.apache.http.protocol.HttpRequestHandler)} */ public void unregister(String pattern) { registry.unregister(pattern); registry.unregister(pattern + "/"); } class Worker extends Thread { final HttpServerConnection htConn; final Socket rawSocket; private volatile boolean closeRequested = false; Worker(HttpServerConnection htConn, Socket rawSocket) { this.htConn = htConn; this.rawSocket = rawSocket; setName("Mock Http Worker: " + rawSocket.getRemoteSocketAddress()); } void stopSocket() { closeRequested = true; try { this.rawSocket.close(); } catch (IOException ex) { // } } private void bail() { this.stopSocket(); } public void doReadLoop() { HttpContext context = new BasicHttpContext(); context.setAttribute(CX_SOCKET, rawSocket); while (!Thread.interrupted() && this.htConn.isOpen() && HttpServer.this.shouldRun) { // Clear the context from any auth settings; since this is done // anew on each connection.. context.removeAttribute(CX_AUTH); try { HttpServer.this.httpService.handleRequest(htConn, context); } catch (ConnectionClosedException ex_closed) { break; } catch (IOException ex) { if (!closeRequested) { ex.printStackTrace(); } break; } catch (HttpException ex) { ex.printStackTrace(); break; } catch (ResponseHandledException ex) { break; } } bail(); } @Override public void run() { try { doReadLoop(); } finally { synchronized (HttpServer.this.allWorkers) { HttpServer.this.allWorkers.remove(this); } bail(); } } } @Override public void run() { setName("Mock HTTP Listener: " + listener.socket().getInetAddress()); while (shouldRun) { Socket incoming; try { incoming = listener.accept().socket(); HttpServerConnection conn = connectionFactory.createConnection(incoming); Worker worker = new Worker(conn, incoming); synchronized (allWorkers) { allWorkers.add(worker); } worker.start(); } catch (IOException ex) { if (shouldRun) { ex.printStackTrace(); } } } } /** Shut down the HTTP server and all its workers, and close the listener socket. */ public void stopServer() { shouldRun = false; try { listener.close(); } catch (IOException ex) { // Don't care } while (true) { synchronized (allWorkers) { if (allWorkers.isEmpty()) { break; } for (Worker w : allWorkers) { w.stopSocket(); w.interrupt(); } } } try { listener.close(); } catch (IOException ex) { ex.printStackTrace(); } } }
/** * Dropwizard Apache Connector. * * <p>It's a custom version of Jersey's {@link org.glassfish.jersey.client.spi.Connector} that uses * Apache's {@link org.apache.http.client.HttpClient} as an HTTP transport implementation. * * <p>It uses a pre-configured HTTP client by {@link io.dropwizard.client.HttpClientBuilder} rather * then creates a client from the Jersey configuration. * * <p>This approach affords to use the extended configuration of the Apache HttpClient in Dropwizard * with a fluent interface of JerseyClient. */ public class DropwizardApacheConnector implements Connector { private static final String APACHE_HTTP_CLIENT_VERSION = VersionInfo.loadVersionInfo( "org.apache.http.client", DropwizardApacheConnector.class.getClassLoader()) .getRelease(); /** Actual HTTP client */ private final CloseableHttpClient client; /** Default HttpUriRequestConfig */ private final RequestConfig defaultRequestConfig; /** Should a chunked encoding be used in POST requests */ private final boolean chunkedEncodingEnabled; public DropwizardApacheConnector( CloseableHttpClient client, RequestConfig defaultRequestConfig, boolean chunkedEncodingEnabled) { this.client = client; this.defaultRequestConfig = defaultRequestConfig; this.chunkedEncodingEnabled = chunkedEncodingEnabled; } /** {@inheritDoc} */ @Override public ClientResponse apply(ClientRequest jerseyRequest) { try { final HttpUriRequest apacheRequest = buildApacheRequest(jerseyRequest); final CloseableHttpResponse apacheResponse = client.execute(apacheRequest); final StatusLine statusLine = apacheResponse.getStatusLine(); final Response.StatusType status = Statuses.from(statusLine.getStatusCode(), firstNonNull(statusLine.getReasonPhrase(), "")); final ClientResponse jerseyResponse = new ClientResponse(status, jerseyRequest); for (Header header : apacheResponse.getAllHeaders()) { final List<String> headerValues = jerseyResponse.getHeaders().get(header.getName()); if (headerValues == null) { jerseyResponse.getHeaders().put(header.getName(), Lists.newArrayList(header.getValue())); } else { headerValues.add(header.getValue()); } } final HttpEntity httpEntity = apacheResponse.getEntity(); jerseyResponse.setEntityStream( httpEntity != null ? httpEntity.getContent() : new ByteArrayInputStream(new byte[0])); return jerseyResponse; } catch (Exception e) { throw new ProcessingException(e); } } /** * Build a new Apache's {@link org.apache.http.client.methods.HttpUriRequest} from Jersey's {@link * org.glassfish.jersey.client.ClientRequest} * * <p>Convert a method, URI, body, headers and override a user-agent if necessary * * @param jerseyRequest representation of an HTTP request in Jersey * @return a new {@link org.apache.http.client.methods.HttpUriRequest} */ private HttpUriRequest buildApacheRequest(ClientRequest jerseyRequest) { final RequestBuilder builder = RequestBuilder.create(jerseyRequest.getMethod()) .setUri(jerseyRequest.getUri()) .setEntity(getHttpEntity(jerseyRequest)); for (String headerName : jerseyRequest.getHeaders().keySet()) { builder.addHeader(headerName, jerseyRequest.getHeaderString(headerName)); } final Optional<RequestConfig> requestConfig = addJerseyRequestConfig(jerseyRequest); requestConfig.ifPresent(builder::setConfig); return builder.build(); } private Optional<RequestConfig> addJerseyRequestConfig(ClientRequest clientRequest) { final Integer timeout = clientRequest.resolveProperty(ClientProperties.READ_TIMEOUT, Integer.class); final Integer connectTimeout = clientRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, Integer.class); final Boolean followRedirects = clientRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, Boolean.class); if (timeout != null || connectTimeout != null || followRedirects != null) { final RequestConfig.Builder requestConfig = RequestConfig.copy(defaultRequestConfig); if (timeout != null) { requestConfig.setSocketTimeout(timeout); } if (connectTimeout != null) { requestConfig.setConnectTimeout(connectTimeout); } if (followRedirects != null) { requestConfig.setRedirectsEnabled(followRedirects); } return Optional.of(requestConfig.build()); } return Optional.empty(); } /** * Get an Apache's {@link org.apache.http.HttpEntity} from Jersey's {@link * org.glassfish.jersey.client.ClientRequest} * * <p>Create a custom HTTP entity, because Jersey doesn't provide a request stream or a byte * buffer. * * @param jerseyRequest representation of an HTTP request in Jersey * @return a correct {@link org.apache.http.HttpEntity} implementation */ private HttpEntity getHttpEntity(ClientRequest jerseyRequest) { if (jerseyRequest.getEntity() == null) { return null; } return chunkedEncodingEnabled ? new JerseyRequestHttpEntity(jerseyRequest) : new BufferedJerseyRequestHttpEntity(jerseyRequest); } /** {@inheritDoc} */ @Override public Future<?> apply(final ClientRequest request, final AsyncConnectorCallback callback) { // Simulate an asynchronous execution return MoreExecutors.newDirectExecutorService() .submit( () -> { try { callback.response(apply(request)); } catch (Exception e) { callback.failure(e); } }); } /** {@inheritDoc} */ @Override public String getName() { return "Apache-HttpClient/" + APACHE_HTTP_CLIENT_VERSION; } /** {@inheritDoc} */ @Override public void close() { // Should not close the client here, because it's managed by the Dropwizard environment } /** * A custom {@link org.apache.http.entity.AbstractHttpEntity} that uses a Jersey request as a * content source. It's chunked because we don't know the content length beforehand. */ private static class JerseyRequestHttpEntity extends AbstractHttpEntity { private ClientRequest clientRequest; private JerseyRequestHttpEntity(ClientRequest clientRequest) { this.clientRequest = clientRequest; setChunked(true); } /** {@inheritDoc} */ @Override public boolean isRepeatable() { return false; } /** {@inheritDoc} */ @Override public long getContentLength() { return -1; } /** * {@inheritDoc} * * <p>This method isn't supported at will throw an {@link * java.lang.UnsupportedOperationException} if invoked. */ @Override public InputStream getContent() throws IOException { // Shouldn't be called throw new UnsupportedOperationException("Reading from the entity is not supported"); } /** {@inheritDoc} */ @Override public void writeTo(final OutputStream outputStream) throws IOException { clientRequest.setStreamProvider(contentLength -> outputStream); clientRequest.writeEntity(); } /** {@inheritDoc} */ @Override public boolean isStreaming() { return false; } } /** * A custom {@link org.apache.http.entity.AbstractHttpEntity} that uses a Jersey request as a * content source. * * <p>In contrast to {@link * io.dropwizard.client.DropwizardApacheConnector.JerseyRequestHttpEntity} its contents are * buffered on initialization. */ private static class BufferedJerseyRequestHttpEntity extends AbstractHttpEntity { private static final int BUFFER_INITIAL_SIZE = 512; private byte[] buffer; private BufferedJerseyRequestHttpEntity(ClientRequest clientRequest) { final ByteArrayOutputStream stream = new ByteArrayOutputStream(BUFFER_INITIAL_SIZE); clientRequest.setStreamProvider(contentLength -> stream); try { clientRequest.writeEntity(); } catch (IOException e) { throw new ProcessingException(LocalizationMessages.ERROR_BUFFERING_ENTITY(), e); } buffer = stream.toByteArray(); setChunked(false); } /** {@inheritDoc} */ @Override public boolean isRepeatable() { return true; } /** {@inheritDoc} */ @Override public long getContentLength() { return buffer.length; } /** * {@inheritDoc} * * <p>This method isn't supported at will throw an {@link * java.lang.UnsupportedOperationException} if invoked. */ @Override public InputStream getContent() throws IOException { // Shouldn't be called throw new UnsupportedOperationException("Reading from the entity is not supported"); } /** {@inheritDoc} */ @Override public void writeTo(OutputStream outstream) throws IOException { outstream.write(buffer); outstream.flush(); } /** {@inheritDoc} */ @Override public boolean isStreaming() { return false; } } }
public String getVersion() { VersionInfo vinfo = VersionInfo.loadVersionInfo( "org.apache.http", Thread.currentThread().getContextClassLoader()); return vinfo.getRelease(); }