Index: trunk/http-core/src/java/org/apache/http/protocol/HttpRequestExecutor.java =================================================================== --- trunk/http-core/src/java/org/apache/http/protocol/HttpRequestExecutor.java (revision 377189) +++ trunk/http-core/src/java/org/apache/http/protocol/HttpRequestExecutor.java (working copy) @@ -35,6 +35,7 @@ import org.apache.http.HttpClientConnection; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpException; +import org.apache.http.HttpHost; import org.apache.http.HttpMutableRequest; import org.apache.http.HttpMutableResponse; import org.apache.http.HttpRequest; @@ -57,31 +58,100 @@ */ public class HttpRequestExecutor extends AbstractHttpProcessor { - private static final int WAIT_FOR_CONTINUE_MS = 10000; + protected static final int WAIT_FOR_CONTINUE_MS = 10000; + + /** The context holding the default context information. */ + protected final HttpContext defaultContext; - private final HttpContext context; - private HttpParams params = null; private HttpRequestRetryHandler retryhandler = null; - + + + /** + * Create a new request executor with default context information. + * The attributes in the argument context will be made available + * in the context used for executing a request. + * + * @param parentContext the default context information, + * or null + */ public HttpRequestExecutor(final HttpContext parentContext) { super(); - this.context = new HttpExecutionContext(parentContext); + this.defaultContext = new HttpExecutionContext(parentContext); } - + + /** + * Create a new request executor. + */ public HttpRequestExecutor() { this(null); } - - public HttpParams getParams() { + + + /** + * Obtain the default context information. + * This is not necessarily the same object passed to the constructor, + * but the default context information will be available here. + * + * @return the context holding the default context information + */ + public final HttpContext getContext() { + return this.defaultContext; + } + + /** + * Obtain the parameters for executing requests. + * + * @return the currently installed parameters + */ + public final HttpParams getParams() { return this.params; } - public void setParams(final HttpParams params) { + /** + * Set new parameters for executing requests. + * + * @param params the new parameters to use from now on + */ + public final void setParams(final HttpParams params) { this.params = params; } - - private boolean canResponseHaveBody(final HttpRequest request, final HttpResponse response) { + + /** + * Obtain the retry handler. + * + * @return the handler deciding whether a request should be retried + */ + public final HttpRequestRetryHandler getRetryHandler() { + return this.retryhandler; + } + + /** + * Set the retry handler. + * + * @param retryhandler the handler to decide whether a request + * should be retried + */ + public final void setRetryHandler(final HttpRequestRetryHandler retryhandler) { + this.retryhandler = retryhandler; + } + + + /** + * Decide whether a response comes with an entity. + * The implementation in this class is based on RFC 2616. + * Unknown methods and response codes are supposed to + * indicate responses with an entity. + *
+ * Derived executors can override this method to handle + * methods and response codes not specified in RFC 2616. + * + * @param request the request, to obtain the executed method + * @param response the response, to obtain the status code + */ + protected boolean canResponseHaveBody(final HttpRequest request, + final HttpResponse response) { + if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) { return false; } @@ -92,57 +162,49 @@ && status != HttpStatus.SC_RESET_CONTENT; } - private HttpMutableResponse doExecute( + + /** + * Send a request and obtain the response, without pre- and postprocessing. + * This method uses the default {@link #getContext context} + * and handles expect-continue handshake if necessary. + * + * @param request the request to send, already + * {@link #prepareRequest prepared} + * @param conn the connection over which to send the request, already + * {@link #prepareConnection prepared} + * + * @throws HttpException in case of a protocol problem + * @throws IOException in case of an IO problem + */ + protected HttpMutableResponse doExecute( final HttpRequest request, final HttpClientConnection conn) throws IOException, HttpException { - HttpMutableResponse response = null; - this.context.setAttribute(HttpExecutionContext.HTTP_REQ_SENT, - new Boolean(false)); - // Send request header - conn.sendRequestHeader(request); - if (request instanceof HttpEntityEnclosingRequest) { - HttpVersion ver = request.getRequestLine().getHttpVersion(); - if (ver.greaterEquals(HttpVersion.HTTP_1_1) - && ((HttpEntityEnclosingRequest)request).expectContinue()) { - // Flush headers - conn.flush(); - if (conn.isResponseAvailable(WAIT_FOR_CONTINUE_MS)) { - response = conn.receiveResponseHeader(this.params); - if (canResponseHaveBody(request, response)) { - conn.receiveResponseEntity(response); - } - int status = response.getStatusLine().getStatusCode(); - if (status < 200) { - if (status != HttpStatus.SC_CONTINUE) { - throw new ProtocolException("Unexpected response: " + - response.getStatusLine()); - } - } else { - return response; - } - } - } - conn.sendRequestEntity((HttpEntityEnclosingRequest) request); - } - conn.flush(); - - this.context.setAttribute(HttpExecutionContext.HTTP_REQ_SENT, - new Boolean(true)); - for (;;) { - // Loop until non 1xx resposne is received - response = conn.receiveResponseHeader(this.params); - if (canResponseHaveBody(request, response)) { - conn.receiveResponseEntity(response); - } - int statuscode = response.getStatusLine().getStatusCode(); - if (statuscode >= HttpStatus.SC_OK) { - break; - } - } + + HttpMutableResponse response = + transmitRequest(request, this.defaultContext, conn, true); + + if (response == null) + response = obtainResponse(request, conn, true); + return response; } - - public HttpResponse execute(final HttpRequest request, final HttpClientConnection conn) + + + /** + * Synchronously send a request and obtain the response. + * + * @param request the request to send. It will be preprocessed. + * @param conn the connection over which to send. + * The {@link HttpClientConnection#setTargetHost target} + * host has to be set before calling this method. + * + * @return the response to the request, postprocessed + * + * @throws HttpException in case of a protocol or processing problem + * @throws IOException in case of an IO problem + */ + public HttpResponse execute(final HttpRequest request, + final HttpClientConnection conn) throws IOException, HttpException { if (request == null) { @@ -151,32 +213,15 @@ if (conn == null) { throw new IllegalArgumentException("Client connection may not be null"); } - this.context.setAttribute(HttpExecutionContext.HTTP_REQUEST, request); - this.context.setAttribute(HttpExecutionContext.HTTP_CONNECTION, conn); - this.context.setAttribute(HttpExecutionContext.HTTP_TARGET_HOST, - conn.getTargetHost()); - // Link own parameters as defaults - request.getParams().setDefaults(this.params); + prepareRequest(request, conn.getTargetHost(), null, null, conn); - if (request instanceof HttpMutableRequest) { - preprocessRequest((HttpMutableRequest)request, this.context); - } - HttpMutableResponse response = null; // loop until the method is successfully processed, the retryHandler // returns false or a non-recoverable exception is thrown for (int execCount = 0; ; execCount++) { try { - if (HttpConnectionParams.isStaleCheckingEnabled(this.params)) { - if (conn.isOpen() && conn.isStale()) { - conn.close(); - } - } - if (!conn.isOpen()) { - conn.open(this.params); - // TODO: Implement secure tunnelling - } + prepareConnection(conn, null, null); response = doExecute(request, conn); // exit retry loop break; @@ -196,16 +241,278 @@ throw ex; } } - postprocessResponse(response, this.context); + + finishResponse(response, null); + return response; - } - public HttpRequestRetryHandler getRetryHandler() { - return this.retryhandler; - } + } // execute - public void setRetryHandler(final HttpRequestRetryHandler retryhandler) { - this.retryhandler = retryhandler; - } - -} + + /** + * Prepare a request for sending. + * + * @param request the request to prepare + * @param target the target host for the request + * @param params the parameters for executing this request, or + * null to use the default + * {@link #getParams parameters} + * @param context the context for sending the request, + * or null to use the default + * {@link #getContext context} + * @param conn the connection for sending the request, + * or null if not yet known + * + * @throws HttpException if the request can not be prepared + * @throws IOException in case of an IO problem + */ + protected void prepareRequest(HttpRequest request, + HttpHost target, + HttpParams params, + HttpContext context, + HttpClientConnection conn) + throws HttpException, IOException { + + if (request == null) + throw new IllegalArgumentException("request must not be null"); + if (request == null) + throw new IllegalArgumentException("target host must not be null"); + + if (params == null) + params = this.params; + if (context == null) + context = this.defaultContext; + + context.setAttribute(HttpExecutionContext.HTTP_REQUEST, request); + //@@@ behavior if proxying - set real target or proxy, or both? + context.setAttribute(HttpExecutionContext.HTTP_TARGET_HOST, target); + if (conn != null) + context.setAttribute(HttpExecutionContext.HTTP_CONNECTION, conn); + else + context.removeAttribute(HttpExecutionContext.HTTP_CONNECTION); + + // link default parameters + request.getParams().setDefaults(params); + + if (request instanceof HttpMutableRequest) + preprocessRequest((HttpMutableRequest) request, context); + + } // prepareRequest + + + /** + * Prepare a connection before sending a request. + * + * @param conn the connection to prepare + * @param target the target host for the request, or + * null to send to the host already + * set as the connection target + * @param params the parameters for preparing the connection, or + * null to use the default + * {@link #getParams parameters} + * + * @throws HttpException in case of a problem + * @throws IOException in case of an IO problem + */ + protected void prepareConnection(HttpClientConnection conn, + HttpHost target, + HttpParams params) + throws HttpException, IOException { + + if (conn == null) + throw new IllegalArgumentException("connection must not be null"); + if (params == null) + params = this.params; + + // make sure the connection is open and points to the target host + if ((target == null) || + target.equals(conn.getTargetHost())) { + + // host and port ok, check whether connection needs to be opened + if (HttpConnectionParams.isStaleCheckingEnabled(params)) { + if (conn.isOpen() && conn.isStale()) { + conn.close(); + } + } + if (!conn.isOpen()) { + conn.open(params); + //TODO: Implement secure tunnelling (@@@ HttpRequestExecutor) + } + + } else { + + // wrong target, point connection to target + if (conn.isOpen()) + conn.close(); + conn.setTargetHost(target); + conn.open(params); + + } // if connection points to target else + + } // prepareConnection + + + /** + * Send a request over a connection. + * This method also handles the expect-continue handshake, if requested. + * + * @param request the request to send, already + * {@link #prepareRequest prepared} + * @param context the context for sending the request, + * or null to use the default + * {@link #getContext context} + * @param conn the connection over which to send the request, already + * {@link #prepareConnection prepared} + * @param exconhs true if an expect-continue handshake + * should be handled, or + * false to disable it. + * If false is passed, this method will + * exclusively send over the connection, otherwise it + * may also have to receive responses. + * + * @return a terminal response received as part of an expect-continue + * handshake. Always null if the exconhs + * argument is false. + * + * @throws HttpException in case of a problem + * @throws IOException in case of an IO problem + */ + protected HttpMutableResponse transmitRequest( + final HttpRequest request, + HttpContext context, + final HttpClientConnection conn, + final boolean exconhs) + throws IOException, HttpException { + + if (request == null) + throw new IllegalArgumentException("request must not be null"); + if (conn == null) + throw new IllegalArgumentException("connection must not be null"); + + if (context == null) + context = this.defaultContext; + + HttpMutableResponse response = null; + context.setAttribute(HttpExecutionContext.HTTP_REQ_SENT, + Boolean.FALSE); + + conn.sendRequestHeader(request); + if (request instanceof HttpEntityEnclosingRequest) { + final HttpEntityEnclosingRequest heer = + (HttpEntityEnclosingRequest) request; + + // Check for expect-continue handshake. We have to flush the + // headers and wait for an 100-continue response to handle it. + // If we get a different response, we must not send the entity. + boolean sendentity = true; + final HttpVersion ver = request.getRequestLine().getHttpVersion(); + if (exconhs && heer.expectContinue() && + ver.greaterEquals(HttpVersion.HTTP_1_1)) { + + conn.flush(); + // As suggested by RFC 2616 section 8.2.3, we don't wait for a + // 100-continue response forever. On timeout, send the entity. + if (conn.isResponseAvailable(WAIT_FOR_CONTINUE_MS)) { + + response = obtainResponse(request, conn, false); + int status = response.getStatusLine().getStatusCode(); + if (status < 200) { + //@@@ TODO: is this in line with RFC 2616, 10.1? + if (status != HttpStatus.SC_CONTINUE) { + throw new ProtocolException + ("Unexpected response: " + + response.getStatusLine()); + } + } else { + sendentity = false; + } + } + } + if (sendentity) + conn.sendRequestEntity(heer); + } + conn.flush(); + + context.setAttribute(HttpExecutionContext.HTTP_REQ_SENT, + Boolean.TRUE); + + return response; + + } // transmitRequest + + + /** + * Wait for and receive a response. + * + * @param request the request for which to obtain the response + * @param conn the connection over which the request was sent + * @param terminal true to wait for a terminal response, or + * false if informational responses with + * status code 1xx should be returned + * + * @return the response, not yet post-processed + * + * @throws HttpException in case of a problem + * @throws IOException in case of an IO problem + */ + protected HttpMutableResponse obtainResponse( + final HttpRequest request, + final HttpClientConnection conn, + final boolean terminal) + throws HttpException, IOException { + + if (request == null) + throw new IllegalArgumentException("request must not be null"); + if (conn == null) + throw new IllegalArgumentException("connection must not be null"); + + // see HttpRequestExecutor.doExecute, final part + HttpMutableResponse response = null; + int statuscode = 0; + + while ((response == null) || + (terminal && (statuscode < HttpStatus.SC_OK))) { + + response = conn.receiveResponseHeader(request.getParams()); + if (canResponseHaveBody(request, response)) { + conn.receiveResponseEntity(response); + } + statuscode = response.getStatusLine().getStatusCode(); + + } // while intermediate response + + return response; + + } // obtainResponse + + + /** + * Finish a response. + * This includes post-processing of the response object. + * It does not read the response entity, if any. + * It does not allow for immediate re-use of the + * connection over which the response is coming in. + * + * @param response the response object to finish + * @param context the context for post-processing the response, or + * null to use the default + * {@link #getContext context} + * + * @throws HttpException in case of a problem + * @throws IOException in case of an IO problem + */ + protected void finishResponse(HttpMutableResponse response, + HttpContext context) + throws HttpException, IOException { + + if (response == null) + throw new IllegalArgumentException("response must not be null"); + if (context == null) + context = this.defaultContext; + + postprocessResponse(response, context); + + } // finishResponse + + +} // class HttpRequestExecutor Index: trunk/http-core/src/examples/org/apache/http/examples/ElementalHttpServer.java =================================================================== --- trunk/http-core/src/examples/org/apache/http/examples/ElementalHttpServer.java (revision 377149) +++ trunk/http-core/src/examples/org/apache/http/examples/ElementalHttpServer.java (working copy) @@ -94,7 +94,7 @@ StringEntity body = new StringEntity("File not found", "UTF-8"); response.setEntity(body); System.out.println("File " + file.getPath() + " not found"); - } else if (!file.canRead()) { + } else if (!file.canRead() || file.isDirectory()) { response.setStatusCode(HttpStatus.SC_FORBIDDEN); StringEntity body = new StringEntity("Access Denied", "UTF-8"); response.setEntity(body); Index: trunk/http-async/src/java/org/apache/http/async/impl/SimpleHttpDispatcher.java =================================================================== --- trunk/http-async/src/java/org/apache/http/async/impl/SimpleHttpDispatcher.java (revision 377149) +++ trunk/http-async/src/java/org/apache/http/async/impl/SimpleHttpDispatcher.java (working copy) @@ -78,6 +78,7 @@ public class SimpleHttpDispatcher extends AbstractHttpDispatcher implements HttpDispatcher { // @@@ move some of this stuff to an abstract base class? + // @@@ the AsyncHttpProcessor and getDefaultContext method, for example /** The one and only connection. */ private final HttpClientConnection client_connection; @@ -227,7 +228,7 @@ // non-javadoc, see interface HttpDispatcher public final HttpContext getDefaultContext() { - return async_processor.getDefaultContext(); + return async_processor.getContext(); } Index: trunk/http-async/src/java/org/apache/http/async/AsyncHttpProcessor.java =================================================================== --- trunk/http-async/src/java/org/apache/http/async/AsyncHttpProcessor.java (revision 377149) +++ trunk/http-async/src/java/org/apache/http/async/AsyncHttpProcessor.java (working copy) @@ -43,7 +43,7 @@ import org.apache.http.HttpException; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpExecutionContext; -import org.apache.http.protocol.AbstractHttpProcessor; +import org.apache.http.protocol.HttpRequestExecutor; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpConnectionParams; @@ -53,30 +53,21 @@ * HTTP processor for asynchronously dispatched requests. * This is the asynchronous equivalent to * {@link org.apache.http.protocol.HttpRequestExecutor HttpRequestExecutor}. - * * * @version $Revision$ $Date$ * * @since 4.0 */ -public class AsyncHttpProcessor extends AbstractHttpProcessor { +public class AsyncHttpProcessor extends HttpRequestExecutor { - /** The default (parent) context. */ - private HttpContext default_context; - /** * Create a new HTTP processor with the given default context. * * @param context the default context */ public AsyncHttpProcessor(HttpContext context) { - - if (context == null) - throw new IllegalArgumentException - ("default context must not be null"); - - default_context = context; + super(context); } @@ -84,26 +75,18 @@ * Create a new HTTP processor with empty default context. */ public AsyncHttpProcessor() { - this(new HttpExecutionContext(null)); + this(null); } - /** - * Obtain the default context. - * - * @return the default context, or null if there is none - */ - public final HttpContext getDefaultContext() { - return default_context; - } - - /** * Prepare a request for sending. * * @param request the request to prepare * @param target the target host for the request - * @param params the default parameters + * @param params the parameters for executing this request, or + * null to use the default + * {@link #getParams parameters} * @param context the parent context for sending the request, * or null to use the default context * @@ -118,27 +101,11 @@ HttpContext context) throws HttpException, IOException { - if (request == null) - throw new IllegalArgumentException("request must not be null"); - if (context == null) - context = default_context; - - // see also HttpRequestExecutor.execute(), initial part - HttpExecutionContext hxc = new HttpExecutionContext(context); - hxc.setAttribute(HttpExecutionContext.HTTP_REQUEST, request); - //@@@ behavior if proxying - set real target or proxy, or both? - hxc.setAttribute(HttpExecutionContext.HTTP_TARGET_HOST, target); - //@@@ Can't set connection in the context, it's not available yet. - //@@@ Is it required for one of the interceptors? + // argument checking is done here... + super.prepareRequest(request, target, params, hxc, null); - // link default parameters - request.getParams().setDefaults(params); - - if (request instanceof HttpMutableRequest) - preprocessRequest((HttpMutableRequest) request, hxc); - return hxc; } // prepareRequest @@ -174,57 +141,15 @@ throw new IllegalStateException ("target host missing in request context"); - // see also HttpRequestExecutor.execute() and .doExecute() - // Retry handling should be implemented by caller, since the retry - // may be triggered by receiving the response, too. + super.prepareConnection(connection, target, request.getParams()); + super.transmitRequest(request, context, connection, + false); // no expect-continue, returns null - // make sure the connection is open and points to the target host - HttpParams params = request.getParams(); - if (target.equals(connection.getTargetHost())) { - - // host and port ok, check whether connection needs to be opened - if (HttpConnectionParams.isStaleCheckingEnabled(params)) { - if (connection.isOpen() && connection.isStale()) { - connection.close(); - } - } - if (!connection.isOpen()) { - connection.open(params); - //TODO: Implement secure tunnelling (@@@ HttpRequestExecutor) - } - - } else { - - // wrong target, point connection to target - if (connection.isOpen()) - connection.close(); - connection.setTargetHost(target); - connection.open(params); - - } // if connection points to target else - - - // this is the initial part of HttpRequestExecutor.doExecute, - // minus the handling of expect/continue handshakes - - context.setAttribute(HttpExecutionContext.HTTP_REQ_SENT, - Boolean.FALSE); - - connection.sendRequestHeader(request); - if (request instanceof HttpEntityEnclosingRequest) { - connection.sendRequestEntity((HttpEntityEnclosingRequest) request); - } - connection.flush(); - - context.setAttribute(HttpExecutionContext.HTTP_REQ_SENT, - Boolean.TRUE); - } // transmitRequest /** * Wait for and receive a response. - * clarify sematics - will the response body be received? * * @param request the request for which to obtain the response * @param connection the connection over which the request was sent @@ -239,59 +164,13 @@ HttpClientConnection connection) throws HttpException, IOException { - if (request == null) - throw new IllegalArgumentException("request must not be null"); - if (connection == null) - throw new IllegalArgumentException("connection must not be null"); + // argument checking is done here... + return super.obtainResponse(request, connection, true); - // see HttpRequestExecutor.doExecute, final part - HttpMutableResponse response = null; - int statuscode = 0; - - // skip 1xx responses - while (statuscode < HttpStatus.SC_OK) { - response = connection.receiveResponseHeader(request.getParams()); - - //@@@ does this actually receive, or just wrap the stream? - //@@@ how to make sure we get only a wrapped stream here? - //@@@ don't call receiveResponseEntity here at all? - //@@@ could there be 1xx responses with an entity? check RFC 2616 - if (canResponseHaveBody(request, response)) { - connection.receiveResponseEntity(response); - } - statuscode = response.getStatusLine().getStatusCode(); - - } // while intermediate response - - return response; - } // obtainResponse /** - * Decide whether a response comes with an entity. - * - * @param request the request responded to - * @param response the response, initialized from headers only - * - * @return true if the response should have an entity, or - * false if it doesn't - */ - //@@@ duplicated from HttpRequestExecutor.canResponseHaveBody - private boolean canResponseHaveBody(final HttpRequest request, - final HttpResponse response) { - if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) { - return false; - } - int status = response.getStatusLine().getStatusCode(); - return status >= HttpStatus.SC_OK - && status != HttpStatus.SC_NO_CONTENT - && status != HttpStatus.SC_NOT_MODIFIED - && status != HttpStatus.SC_RESET_CONTENT; - } - - - /** * Finish a response. * This includes post-processing of the response object. * It does not read the response entity (if any), nor allows @@ -309,13 +188,9 @@ HttpContext context) throws HttpException, IOException { - if (response == null) - throw new IllegalArgumentException("response must not be null"); - if (context == null) - throw new IllegalArgumentException("context must not be null"); + // argument checking is done here... + super.finishResponse(response, context); - postprocessResponse(response, context); - } // finishResponse Index: trunk/http-async/src/examples/org/apache/http/examples/ElementalAsyncGet.java =================================================================== --- trunk/http-async/src/examples/org/apache/http/examples/ElementalAsyncGet.java (revision 377149) +++ trunk/http-async/src/examples/org/apache/http/examples/ElementalAsyncGet.java (working copy) @@ -80,12 +80,12 @@ if ((targets == null) || (targets.length < 1)) { targets = new String[] { "/", - "/manual/", + "/servlets-examples/servlet/RequestInfoExample", "/somewhere%20in%20pampa" }; } - HttpHost host = new HttpHost("localhost", 80); + HttpHost host = new HttpHost("localhost", 8080); HttpHandle[] handles = new HttpHandle[targets.length]; for (int i = 0; i < targets.length; i++) {