diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2ConscriptRequestExecutionExample.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2ConscriptRequestExecutionExample.java index 30f27d777..b86df1509 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2ConscriptRequestExecutionExample.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2ConscriptRequestExecutionExample.java @@ -42,9 +42,8 @@ import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; import org.apache.hc.core5.http.nio.AsyncClientEndpoint; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; -import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.nio.support.AsyncClientPipeline; +import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.http2.frame.RawFrame; @@ -127,43 +126,46 @@ public void onOutputFlowControl(final HttpConnection connection, final int strea final Future future = requester.connect(target, Timeout.ofDays(5)); final AsyncClientEndpoint clientEndpoint = future.get(); - final String[] requestUris = new String[] {"/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"}; + final String[] requestUris = new String[]{"/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"}; final CountDownLatch latch = new CountDownLatch(requestUris.length); - for (final String requestUri: requestUris) { + for (final String requestUri : requestUris) { clientEndpoint.execute( - AsyncRequestBuilder.get() - .setHttpHost(target) - .setPath(requestUri) - .build(), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - new FutureCallback>() { - - @Override - public void completed(final Message message) { - clientEndpoint.releaseAndReuse(); - final HttpResponse response = message.head(); - final String body = message.body(); - System.out.println(requestUri + "->" + response.getCode() + " " + response.getVersion()); - System.out.println(body); - latch.countDown(); - } - - @Override - public void failed(final Exception ex) { - clientEndpoint.releaseAndDiscard(); - System.out.println(requestUri + "->" + ex); - latch.countDown(); - } - - @Override - public void cancelled() { - clientEndpoint.releaseAndDiscard(); - System.out.println(requestUri + " cancelled"); - latch.countDown(); - } - - }); + AsyncClientPipeline.assemble() + .request() + .get(target, requestUri) + .response() + .asString() + .result(new FutureCallback>() { + + @Override + public void completed(final Message message) { + clientEndpoint.releaseAndReuse(); + final HttpResponse response = message.head(); + final String body = message.body(); + System.out.println(requestUri + "->" + response.getCode() + " " + response.getVersion()); + System.out.println(body); + latch.countDown(); + } + + @Override + public void failed(final Exception ex) { + clientEndpoint.releaseAndDiscard(); + System.out.println(requestUri + "->" + ex); + latch.countDown(); + } + + @Override + public void cancelled() { + clientEndpoint.releaseAndDiscard(); + System.out.println(requestUri + " cancelled"); + latch.countDown(); + } + + }) + .create(), + null, + HttpCoreContext.create()); } latch.await(); diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2FileServerExample.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2FileServerExample.java index abd8c90f6..ab9848297 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2FileServerExample.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2FileServerExample.java @@ -27,34 +27,36 @@ package org.apache.hc.core5.http2.examples; import java.io.File; -import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.util.List; +import java.util.Objects; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.EndpointDetails; -import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpConnection; -import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; -import org.apache.hc.core5.http.nio.AsyncRequestConsumer; -import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; -import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; -import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer; -import org.apache.hc.core5.http.nio.support.AsyncResponseBuilder; -import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; -import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.impl.routing.RequestRouter; +import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; +import org.apache.hc.core5.http.nio.entity.FileEntityProducer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; +import org.apache.hc.core5.http.nio.support.AsyncServerPipeline; +import org.apache.hc.core5.http.nio.support.BasicResponseProducer; import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.http.protocol.HttpDateGenerator; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.frame.RawFrame; import org.apache.hc.core5.http2.impl.nio.H2StreamListener; @@ -87,6 +89,59 @@ public static void main(final String[] args) throws Exception { .setTcpNoDelay(true) .build(); + final Supplier exchangeHandlerSupplier = AsyncServerPipeline.assemble() + // Read GET / HEAD requests ignoring their content body + .request(Method.GET, Method.HEAD) + .ignoreContent() + // Write out responses by streaming out content of a file + .response() + .>produce(m -> { + if (m.error() == null) { + final File file = m.getBody(); + final ContentType contentType; + final String filename = TextUtils.toLowerCase(file.getName()); + if (filename.endsWith(".txt")) { + contentType = ContentType.TEXT_PLAIN; + } else if (filename.endsWith(".html") || filename.endsWith(".htm")) { + contentType = ContentType.TEXT_HTML; + } else if (filename.endsWith(".xml")) { + contentType = ContentType.TEXT_XML; + } else { + contentType = ContentType.DEFAULT_BINARY; + } + return new BasicResponseProducer(new FileEntityProducer(file, contentType)); + } else { + return new BasicResponseProducer(new StringAsyncEntityProducer(Objects.toString(m.error()), ContentType.TEXT_PLAIN)); + } + }) + // Map exceptions to a response message + .errorMessage(Throwable::getMessage) + // Generate a response to the request + .handle((m, context) -> { + final HttpRequest request = m.head(); + final URI requestUri; + try { + requestUri = request.getUri(); + } catch (final URISyntaxException ex) { + throw new ProtocolException(ex.getMessage(), ex); + } + final String path = requestUri.getPath(); + final File file = new File(docRoot, path); + if (!file.exists()) { + println("File " + file.getPath() + " not found"); + return Message.error(new BasicHttpResponse(HttpStatus.SC_NOT_FOUND), "File not found"); + } else if (!file.canRead() || file.isDirectory()) { + println("Cannot read file " + file.getPath()); + return Message.error(new BasicHttpResponse(HttpStatus.SC_FORBIDDEN), "File cannot be accessed"); + } else { + final HttpCoreContext coreContext = HttpCoreContext.cast(context); + final EndpointDetails endpoint = coreContext.getEndpointDetails(); + println(endpoint + " | serving file " + file.getPath()); + return Message.of(new BasicHttpResponse(HttpStatus.SC_OK), file); + } + }) + .supplier(); + final HttpAsyncServer server = H2ServerBootstrap.bootstrap() .setIOReactorConfig(config) .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2) @@ -123,75 +178,10 @@ public void onOutputFlowControl(final HttpConnection connection, final int strea } }) - .register("*", new AsyncServerRequestHandler>() { - - @Override - public AsyncRequestConsumer> prepare( - final HttpRequest request, - final EntityDetails entityDetails, - final HttpContext context) throws HttpException { - return new BasicRequestConsumer<>(entityDetails != null ? new DiscardingEntityConsumer<>() : null); - } - - @Override - public void handle( - final Message message, - final ResponseTrigger responseTrigger, - final HttpContext localContext) throws HttpException, IOException { - final HttpCoreContext context = HttpCoreContext.cast(localContext); - final HttpRequest request = message.head(); - final URI requestUri; - try { - requestUri = request.getUri(); - } catch (final URISyntaxException ex) { - throw new ProtocolException(ex.getMessage(), ex); - } - final String path = requestUri.getPath(); - final File file = new File(docRoot, path); - if (!file.exists()) { - - System.out.println("File " + file.getPath() + " not found"); - responseTrigger.submitResponse( - AsyncResponseBuilder.create(HttpStatus.SC_NOT_FOUND) - .setEntity("

File" + file.getPath() + - " not found

", ContentType.TEXT_HTML) - .build(), - context); - - } else if (!file.canRead() || file.isDirectory()) { - - System.out.println("Cannot read file " + file.getPath()); - responseTrigger.submitResponse( - AsyncResponseBuilder.create(HttpStatus.SC_FORBIDDEN) - .setEntity("

Access denied

", ContentType.TEXT_HTML) - .build(), - context); - - } else { - - final ContentType contentType; - final String filename = TextUtils.toLowerCase(file.getName()); - if (filename.endsWith(".txt")) { - contentType = ContentType.TEXT_PLAIN; - } else if (filename.endsWith(".html") || filename.endsWith(".htm")) { - contentType = ContentType.TEXT_HTML; - } else if (filename.endsWith(".xml")) { - contentType = ContentType.TEXT_XML; - } else { - contentType = ContentType.DEFAULT_BINARY; - } - - final EndpointDetails endpoint = context.getEndpointDetails(); - System.out.println(endpoint + ": serving file " + file.getPath()); - responseTrigger.submitResponse( - AsyncResponseBuilder.create(HttpStatus.SC_OK) - .setEntity(AsyncEntityProducers.create(file, contentType)) - .build(), - context); - } - } - - }) + .setRequestRouter(RequestRouter.>builder() + .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", exchangeHandlerSupplier) + .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) + .build()) .create(); Runtime.getRuntime().addShutdownHook(new Thread(() -> { @@ -206,4 +196,8 @@ public void handle( server.awaitShutdown(TimeValue.ofDays(Long.MAX_VALUE)); } + static void println(final String msg) { + System.out.println(HttpDateGenerator.INSTANCE.getCurrentDate() + " | " + msg); + } + } diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java index bb13ea01f..f854aee6e 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2GreetingServer.java @@ -26,43 +26,34 @@ */ package org.apache.hc.core5.http2.examples; -import java.io.IOException; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.EndpointDetails; -import org.apache.hc.core5.http.EntityDetails; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; +import org.apache.hc.core5.http.impl.routing.RequestRouter; import org.apache.hc.core5.http.message.BasicHttpResponse; -import org.apache.hc.core5.http.nio.AsyncEntityConsumer; -import org.apache.hc.core5.http.nio.AsyncRequestConsumer; -import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; -import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer; import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; import org.apache.hc.core5.http.nio.support.AbstractServerExchangeHandler; -import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; -import org.apache.hc.core5.http.nio.support.BasicResponseProducer; -import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.nio.support.AsyncServerPipeline; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http2.HttpVersionPolicy; -import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.http2.impl.nio.bootstrap.H2ServerBootstrap; import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.net.WWWFormCodec; -import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.reactor.ListenerEndpoint; import org.apache.hc.core5.util.TimeValue; @@ -90,16 +81,58 @@ public static void main(final String[] args) throws ExecutionException, Interrup port = Integer.parseInt(args[0]); } + final Supplier exchangeHandlerSupplier = AsyncServerPipeline.assemble() + // Represent request as string + .request() + .consumeContent(contentType -> { + if (contentType != null && contentType.isSameMimeType(ContentType.APPLICATION_FORM_URLENCODED)) { + return StringAsyncEntityConsumer::new; + } else { + // Discard content that cannot be correctly processed + return DiscardingEntityConsumer::new; + } + }) + // Represent response as string + .response() + .asString(ContentType.TEXT_PLAIN) + // Generate a response to the request + .handle((r, c) -> { + final HttpCoreContext context = HttpCoreContext.cast(c); + final EndpointDetails endpoint = context.getEndpointDetails(); + final HttpRequest req = r.head(); + final String httpEntity = r.body(); + + // recording the request + System.out.printf("[%s] %s %s %s%n", Instant.now(), + endpoint != null ? endpoint.getRemoteAddress() : null, + req.getMethod(), + req.getPath()); + + String name = "stranger"; + if (httpEntity != null) { + // decoding the form entity into key/value pairs: + final List params = WWWFormCodec.parse(httpEntity, StandardCharsets.UTF_8); + if (!params.isEmpty()) { + name = params.get(0).getValue(); + } + } + + // composing greeting: + final String greeting = String.format("Hello %s\n", name); + return Message.of(new BasicHttpResponse(HttpStatus.SC_OK), greeting); + + }) + .supplier(); + final HttpAsyncServer server = H2ServerBootstrap.bootstrap() - .setH2Config(H2Config.DEFAULT) - .setIOReactorConfig(IOReactorConfig.DEFAULT) .setVersionPolicy(HttpVersionPolicy.NEGOTIATE) // fallback to HTTP/1 as needed - - // wildcard path matcher: - .register("*", CustomServerExchangeHandler::new) + .setRequestRouter(RequestRouter.>builder() + // wildcard path matcher: + .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", exchangeHandlerSupplier) + .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) + .build()) .create(); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("HTTP server shutting down"); server.close(CloseMode.GRACEFUL); @@ -112,70 +145,5 @@ public static void main(final String[] args) throws ExecutionException, Interrup server.awaitShutdown(TimeValue.ofDays(Long.MAX_VALUE)); } - static class CustomServerExchangeHandler extends AbstractServerExchangeHandler> { - - - @Override - protected AsyncRequestConsumer> supplyConsumer( - final HttpRequest request, - final EntityDetails entityDetails, - final HttpContext context) { - // if there's no body don't try to parse entity: - AsyncEntityConsumer entityConsumer = new DiscardingEntityConsumer<>(); - - if (entityDetails != null) { - entityConsumer = new StringAsyncEntityConsumer(); - } - //noinspection unchecked - return new BasicRequestConsumer<>(entityConsumer); - - } - - @Override - protected void handle(final Message requestMessage, - final AsyncServerRequestHandler.ResponseTrigger responseTrigger, - final HttpContext localContext) throws HttpException, IOException { - - final HttpCoreContext context = HttpCoreContext.cast(localContext); - final EndpointDetails endpoint = context.getEndpointDetails(); - final HttpRequest req = requestMessage.head(); - final String httpEntity = requestMessage.body(); - - // generic success response: - final HttpResponse resp = new BasicHttpResponse(200); - - // recording the request - System.out.printf("[%s] %s %s %s%n", Instant.now(), - endpoint != null ? endpoint.getRemoteAddress() : null, - req.getMethod(), - req.getPath()); - - // Request without an entity - GET/HEAD/DELETE - if (httpEntity == null) { - responseTrigger.submitResponse( - new BasicResponseProducer(resp), context); - return; - } - - // Request with an entity - POST/PUT - final Header cth = req.getHeader(HttpHeaders.CONTENT_TYPE); - final ContentType contentType = cth != null ? ContentType.parse(cth.getValue()) : null; - String name = "stranger"; - if (contentType != null && contentType.isSameMimeType(ContentType.APPLICATION_FORM_URLENCODED)) { - - // decoding the form entity into key/value pairs: - final List args = WWWFormCodec.parse(httpEntity, contentType.getCharset()); - if (!args.isEmpty()) { - name = args.get(0).getValue(); - } - } - - // composing greeting: - final String greeting = String.format("Hello %s\n", name); - responseTrigger.submitResponse( - new BasicResponseProducer(resp, AsyncEntityProducers.create(greeting)), context); - } - } - } diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2MultiStreamExecutionExample.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2MultiStreamExecutionExample.java index 9c878fdff..b4bddb484 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2MultiStreamExecutionExample.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2MultiStreamExecutionExample.java @@ -39,9 +39,8 @@ import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; import org.apache.hc.core5.http.nio.AsyncClientEndpoint; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; -import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.nio.support.AsyncClientPipeline; +import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.http2.frame.RawFrame; @@ -113,43 +112,46 @@ public void onOutputFlowControl(final HttpConnection connection, final int strea requester.start(); final HttpHost target = new HttpHost("nghttp2.org"); - final String[] requestUris = new String[] {"/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"}; - final Future future = requester.connect(target, Timeout.ofSeconds(5)); + final Future future = requester.connect(target, Timeout.ofSeconds(30)); final AsyncClientEndpoint clientEndpoint = future.get(); + final String[] requestUris = new String[]{"/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"}; final CountDownLatch latch = new CountDownLatch(requestUris.length); - for (final String requestUri: requestUris) { + for (final String requestUri : requestUris) { clientEndpoint.execute( - AsyncRequestBuilder.get() - .setHttpHost(target) - .setPath(requestUri) - .build(), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - new FutureCallback>() { - - @Override - public void completed(final Message message) { - latch.countDown(); - final HttpResponse response = message.head(); - final String body = message.body(); - System.out.println(requestUri + "->" + response.getCode()); - System.out.println(body); - } - - @Override - public void failed(final Exception ex) { - latch.countDown(); - System.out.println(requestUri + "->" + ex); - } - - @Override - public void cancelled() { - latch.countDown(); - System.out.println(requestUri + " cancelled"); - } - - }); + AsyncClientPipeline.assemble() + .request() + .get(target, requestUri) + .response() + .asString() + .result(new FutureCallback>() { + + @Override + public void completed(final Message m) { + latch.countDown(); + final HttpResponse response = m.head(); + final String body = m.body(); + System.out.println(requestUri + "->" + response.getCode()); + System.out.println(body); + } + + @Override + public void failed(final Exception ex) { + latch.countDown(); + System.out.println(requestUri + "->" + ex); + } + + @Override + public void cancelled() { + latch.countDown(); + System.out.println(requestUri + " cancelled"); + } + + }) + .create(), + null, + HttpCoreContext.create()); } latch.await(); diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionExample.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionExample.java index e7dab86af..e2f88fbed 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionExample.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionExample.java @@ -38,9 +38,8 @@ import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; import org.apache.hc.core5.http.nio.AsyncClientEndpoint; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; -import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.nio.support.AsyncClientPipeline; +import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.http2.frame.RawFrame; @@ -105,46 +104,48 @@ public void onOutputFlowControl(final HttpConnection connection, final int strea requester.start(); final HttpHost target = new HttpHost("nghttp2.org"); - final Future future = requester.connect(target, Timeout.ofSeconds(5)); + final Future future = requester.connect(target, Timeout.ofSeconds(30)); final AsyncClientEndpoint clientEndpoint = future.get(); final String[] requestUris = new String[] {"/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"}; - final CountDownLatch latch = new CountDownLatch(requestUris.length); for (final String requestUri: requestUris) { clientEndpoint.execute( - AsyncRequestBuilder.get() - .setHttpHost(target) - .setPath(requestUri) - .build(), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - new FutureCallback>() { - - @Override - public void completed(final Message message) { - clientEndpoint.releaseAndReuse(); - final HttpResponse response = message.head(); - final String body = message.body(); - System.out.println(requestUri + "->" + response.getCode()); - System.out.println(body); - latch.countDown(); - } - - @Override - public void failed(final Exception ex) { - clientEndpoint.releaseAndDiscard(); - System.out.println(requestUri + "->" + ex); - latch.countDown(); - } - - @Override - public void cancelled() { - clientEndpoint.releaseAndDiscard(); - System.out.println(requestUri + " cancelled"); - latch.countDown(); - } - - }); + AsyncClientPipeline.assemble() + .request() + .get(target, requestUri) + .response() + .asString() + .result(new FutureCallback>() { + + @Override + public void completed(final Message m) { + clientEndpoint.releaseAndReuse(); + final HttpResponse response = m.head(); + final String body = m.body(); + System.out.println(requestUri + "->" + response.getCode()); + System.out.println(body); + latch.countDown(); + } + + @Override + public void failed(final Exception ex) { + clientEndpoint.releaseAndDiscard(); + System.out.println(requestUri + "->" + ex); + latch.countDown(); + } + + @Override + public void cancelled() { + clientEndpoint.releaseAndDiscard(); + System.out.println(requestUri + " cancelled"); + latch.countDown(); + } + + }) + .create(), + null, + HttpCoreContext.create()); } latch.await(); diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionWithPriorityExample.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionWithPriorityExample.java index 721050758..fe6d0f96c 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionWithPriorityExample.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/examples/H2RequestExecutionWithPriorityExample.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.apache.hc.core5.annotation.Experimental; import org.apache.hc.core5.concurrent.FutureCallback; @@ -40,19 +41,18 @@ import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; import org.apache.hc.core5.http.message.BasicHttpRequest; import org.apache.hc.core5.http.nio.AsyncClientEndpoint; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.support.BasicRequestProducer; -import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.nio.support.AsyncClientPipeline; +import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.support.BasicRequestBuilder; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.http2.frame.RawFrame; -import org.apache.hc.core5.http2.impl.H2Processors; import org.apache.hc.core5.http2.impl.nio.H2StreamListener; import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap; import org.apache.hc.core5.http2.priority.PriorityFormatter; import org.apache.hc.core5.http2.priority.PriorityValue; import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.util.Timeout; /** @@ -63,45 +63,51 @@ public class H2RequestExecutionWithPriorityExample { public static void main(final String[] args) throws Exception { - // Force HTTP/2 and disable push for a cleaner demo + // Create and start requester + final IOReactorConfig ioReactorConfig = IOReactorConfig.custom() + .setSoTimeout(5, TimeUnit.SECONDS) + .build(); + final H2Config h2Config = H2Config.custom() .setPushEnabled(false) .build(); final HttpAsyncRequester requester = H2RequesterBootstrap.bootstrap() - .setH2Config(h2Config) + .setIOReactorConfig(ioReactorConfig) .setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2) - .setHttpProcessor(H2Processors.client()) // includes H2RequestPriority + .setH2Config(h2Config) .setStreamListener(new H2StreamListener() { + @Override public void onHeaderInput(final HttpConnection connection, final int streamId, final List headers) { - for (final Header h : headers) { - System.out.println(connection.getRemoteAddress() + " (" + streamId + ") << " + h); + for (int i = 0; i < headers.size(); i++) { + System.out.println(connection.getRemoteAddress() + " (" + streamId + ") << " + headers.get(i)); } } @Override public void onHeaderOutput(final HttpConnection connection, final int streamId, final List headers) { - for (final Header h : headers) { - System.out.println(connection.getRemoteAddress() + " (" + streamId + ") >> " + h); + for (int i = 0; i < headers.size(); i++) { + System.out.println(connection.getRemoteAddress() + " (" + streamId + ") >> " + headers.get(i)); } } @Override - public void onFrameInput(final HttpConnection c, final int id, final RawFrame f) { + public void onFrameInput(final HttpConnection connection, final int streamId, final RawFrame frame) { } @Override - public void onFrameOutput(final HttpConnection c, final int id, final RawFrame f) { + public void onFrameOutput(final HttpConnection connection, final int streamId, final RawFrame frame) { } @Override - public void onInputFlowControl(final HttpConnection c, final int id, final int d, final int s) { + public void onInputFlowControl(final HttpConnection connection, final int streamId, final int delta, final int actualSize) { } @Override - public void onOutputFlowControl(final HttpConnection c, final int id, final int d, final int s) { + public void onOutputFlowControl(final HttpConnection connection, final int streamId, final int delta, final int actualSize) { } + }) .create(); @@ -123,6 +129,8 @@ public void onOutputFlowControl(final HttpConnection c, final int id, final int // ---- Request 2: RFC defaults -> header MUST be omitted by the interceptor executeWithPriority(clientEndpoint, target, "/httpbin/user-agent", PriorityValue.defaults(), latch); + latch.await(); + System.out.println("Shutting down I/O reactor"); requester.initiateShutdown(); } @@ -143,34 +151,41 @@ private static void executeWithPriority( } clientEndpoint.execute( - new BasicRequestProducer(request, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - new FutureCallback>() { - - @Override - public void completed(final Message message) { - clientEndpoint.releaseAndReuse(); - final HttpResponse response = message.head(); - final String body = message.body(); - System.out.println(requestUri + "->" + response.getCode()); - System.out.println(body); - latch.countDown(); - } - - @Override - public void failed(final Exception ex) { - clientEndpoint.releaseAndDiscard(); - System.out.println(requestUri + "->" + ex); - latch.countDown(); - } - - @Override - public void cancelled() { - clientEndpoint.releaseAndDiscard(); - System.out.println(requestUri + " cancelled"); - latch.countDown(); - } + AsyncClientPipeline.assemble() + .request(request) + .noContent() + .response() + .asString() + .result(new FutureCallback>() { + + @Override + public void completed(final Message message) { + clientEndpoint.releaseAndReuse(); + final HttpResponse response = message.head(); + final String body = message.body(); + System.out.println(requestUri + "->" + response.getCode()); + System.out.println(body); + latch.countDown(); + } + + @Override + public void failed(final Exception ex) { + clientEndpoint.releaseAndDiscard(); + System.out.println(requestUri + "->" + ex); + latch.countDown(); + } + + @Override + public void cancelled() { + clientEndpoint.releaseAndDiscard(); + System.out.println(requestUri + " cancelled"); + latch.countDown(); + } + + }) + .create(), + null, + HttpCoreContext.create()); - }); } } \ No newline at end of file diff --git a/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AbstractJsonEntityConsumer.java b/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AbstractJsonEntityConsumer.java index 48d54c79f..11e5d36ea 100644 --- a/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AbstractJsonEntityConsumer.java +++ b/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AbstractJsonEntityConsumer.java @@ -39,10 +39,10 @@ import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.UnsupportedMediaTypeException; import org.apache.hc.core5.http.nio.AsyncEntityConsumer; import org.apache.hc.core5.http.nio.CapacityChannel; import org.apache.hc.core5.jackson2.JsonAsyncTokenizer; -import org.apache.hc.core5.jackson2.JsonContentException; import org.apache.hc.core5.jackson2.JsonTokenConsumer; import org.apache.hc.core5.util.Args; @@ -64,7 +64,7 @@ abstract class AbstractJsonEntityConsumer implements AsyncEntityConsumer { public final void streamStart(final EntityDetails entityDetails, final FutureCallback resultCallback) throws HttpException, IOException { final ContentType contentType = ContentType.parseLenient(entityDetails.getContentType()); if (contentType != null && !ContentType.APPLICATION_JSON.isSameMimeType(contentType)) { - throw new JsonContentException("Unexpected content type: " + contentType.getMimeType()); + throw new UnsupportedMediaTypeException(contentType); } resultCallbackRef.set(resultCallback); jsonTokenizer.initialize(createJsonTokenConsumer(result -> { diff --git a/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AbstractJsonMessageConsumer.java b/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AbstractJsonMessageConsumer.java index 86e9b98ea..9eaf490fc 100644 --- a/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AbstractJsonMessageConsumer.java +++ b/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AbstractJsonMessageConsumer.java @@ -39,11 +39,11 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpMessage; +import org.apache.hc.core5.http.UnsupportedMediaTypeException; import org.apache.hc.core5.http.nio.AsyncDataConsumer; import org.apache.hc.core5.http.nio.AsyncEntityConsumer; import org.apache.hc.core5.http.nio.CapacityChannel; import org.apache.hc.core5.http.protocol.HttpContext; -import org.apache.hc.core5.jackson2.JsonContentException; import org.apache.hc.core5.util.Args; abstract class AbstractJsonMessageConsumer implements AsyncDataConsumer { @@ -67,6 +67,11 @@ protected void consumeMessage(final H messageHead, final EntityDetails entityDetails, final HttpContext context, final FutureCallback resultCallback) throws HttpException, IOException { + if (entityDetails == null) { + resultCallback.completed(null); + return; + } + final ContentType contentType = ContentType.parseLenient(entityDetails.getContentType()); if (contentType == null || ContentType.APPLICATION_JSON.isSameMimeType(contentType)) { final AsyncEntityConsumer entityConsumer = jsonConsumerSupplier.get(); @@ -86,7 +91,7 @@ public void completed(final T result) { @Override public void completed(final T ignore) { - resultCallback.failed(new JsonContentException("Unexpected content type: " + contentType.getMimeType())); + resultCallback.failed(new UnsupportedMediaTypeException(contentType)); } }); diff --git a/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AsyncJsonClientPipeline.java b/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AsyncJsonClientPipeline.java new file mode 100644 index 000000000..a69b8f6a2 --- /dev/null +++ b/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AsyncJsonClientPipeline.java @@ -0,0 +1,467 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.jackson2.http; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Callback; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HandlerResolver; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.support.AbstractClientExchangeHandler; +import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.support.BasicRequestBuilder; +import org.apache.hc.core5.jackson2.JsonConsumer; +import org.apache.hc.core5.jackson2.JsonResultSink; +import org.apache.hc.core5.jackson2.JsonTokenEventHandler; +import org.apache.hc.core5.util.Args; + +/** + * Client side execution pipeline assembler that creates {@link AsyncClientExchangeHandler} instances + * with the defined message exchange pipeline optimized for JSON message exchanges that triggers + * the given {@link FutureCallback} or {@link CompletableFuture} upon completion. + *

+ * Please note that {@link AsyncClientExchangeHandler} are stateful and may not be used concurrently + * by multiple message exchanges or re-used for subsequent message exchanges. + * + * @since 5.5 + */ +public final class AsyncJsonClientPipeline { + + private static final String JSON_CONTENT_TYPE_TEXT = ContentType.APPLICATION_JSON.toString(); + private final ObjectMapper objectMapper; + + private AsyncJsonClientPipeline(final ObjectMapper objectMapper) { + this.objectMapper = Args.notNull(objectMapper, "Object mapper"); + } + + public static AsyncJsonClientPipeline assemble(final ObjectMapper objectMapper) { + return new AsyncJsonClientPipeline(objectMapper); + } + + public RequestStage request() { + return new RequestStage(); + } + + /** + * Configures the pipeline to produce an outgoing message stream with the given + * request head. + */ + public RequestContentStage request(final HttpRequest request) { + return new RequestContentStage(request); + } + + /** + * Request message stage. + */ + public class RequestStage { + + private RequestStage() { + } + + /** + * Configures {@link AsyncRequestProducer} to be used by the pipeline to generate the outgoing + * request message stream. + */ + public ResponseStage produce(final AsyncRequestProducer requestProducer) { + return new ResponseStage(requestProducer); + } + + /** + * Configures the pipeline to produce an outgoing GET message stream. + */ + public ResponseStage get(final URI requestUri) { + return new ResponseStage(AsyncRequestBuilder.get(requestUri) + .addHeader(HttpHeaders.ACCEPT, JSON_CONTENT_TYPE_TEXT) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing GET message stream. + */ + public ResponseStage get(final HttpHost target, final String path) { + return new ResponseStage(AsyncRequestBuilder.get() + .setHttpHost(target) + .setPath(path) + .addHeader(HttpHeaders.ACCEPT, JSON_CONTENT_TYPE_TEXT) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing POST message stream. + */ + public RequestContentStage post(final URI requestUri) { + return new RequestContentStage(BasicRequestBuilder.post(requestUri) + .addHeader(HttpHeaders.ACCEPT, JSON_CONTENT_TYPE_TEXT) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing POST message stream. + */ + public RequestContentStage post(final HttpHost target, final String path) { + return new RequestContentStage(BasicRequestBuilder.post() + .setHttpHost(target) + .setPath(path) + .addHeader(HttpHeaders.ACCEPT, JSON_CONTENT_TYPE_TEXT) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing PUT message stream. + */ + public RequestContentStage put(final URI requestUri) { + return new RequestContentStage(BasicRequestBuilder.put(requestUri) + .addHeader(HttpHeaders.ACCEPT, JSON_CONTENT_TYPE_TEXT) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing PUT message stream. + */ + public RequestContentStage put(final HttpHost target, final String path) { + return new RequestContentStage(BasicRequestBuilder.put() + .setHttpHost(target) + .setPath(path) + .addHeader(HttpHeaders.ACCEPT, JSON_CONTENT_TYPE_TEXT) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing PATCH message stream. + */ + public RequestContentStage patch(final URI requestUri) { + return new RequestContentStage(BasicRequestBuilder.patch(requestUri) + .addHeader(HttpHeaders.ACCEPT, JSON_CONTENT_TYPE_TEXT) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing PATCH message stream. + */ + public RequestContentStage patch(final HttpHost target, final String path) { + return new RequestContentStage(BasicRequestBuilder.patch() + .setHttpHost(target) + .setPath(path) + .addHeader(HttpHeaders.ACCEPT, JSON_CONTENT_TYPE_TEXT) + .build()); + } + + } + + /** + * Request content stage. + */ + public class RequestContentStage { + + private final HttpRequest request; + + private RequestContentStage(final HttpRequest request) { + this.request = request; + } + + /** + * Configures {@link AsyncEntityProducer} to be used by the pipeline to generate the outgoing + * request content stream. + */ + public ResponseStage produceContent(final AsyncEntityProducer dataProducer) { + return new ResponseStage(new BasicRequestProducer(request, dataProducer)); + } + + /** + * Configures the pipeline to represent the outgoing message content as a byte array. + */ + public ResponseStage asObject(final T content) { + return new ResponseStage(JsonRequestProducers.create(request, content, objectMapper)); + } + + /** + * Configures the pipeline to represent the outgoing message content as a byte array. + */ + public ResponseStage asJsonNode(final JsonNode content) { + return new ResponseStage(JsonRequestProducers.create(request, content, objectMapper)); + } + + /** + * Configures the pipeline to represent the outgoing message content as a sequence + * of objects. + */ + public ResponseStage asSequence(final ObjectProducer objectProducer) { + return new ResponseStage(JsonRequestProducers.create(request, objectMapper, objectProducer)); + } + + /** + * Configures the pipeline to represent the outgoing message without a content body. + */ + public ResponseStage noContent() { + return produceContent(null); + } + + } + + /** + * Response message stage. + */ + public class ResponseStage { + + private final AsyncRequestProducer requestProducer; + + private ResponseStage(final AsyncRequestProducer requestProducer) { + this.requestProducer = requestProducer; + } + + /** + * Configures the pipeline to processes the incoming response message stream. + */ + public ResponseContentStage response() { + return new ResponseContentStage(requestProducer); + } + + } + + /** + * Response content generation stage. + */ + public class ResponseContentStage { + + private final AsyncRequestProducer requestProducer; + + private ResponseContentStage(final AsyncRequestProducer requestProducer) { + this.requestProducer = requestProducer; + } + + /** + * Configures {@link AsyncResponseConsumer} to be used by the pipeline to process + * the incoming response message stream. + * + * @param response content representation. + */ + public ResultStage consume(final HandlerResolver> responseConsumerResolver) { + return new ResultStage<>(requestProducer, responseConsumerResolver); + } + + /** + * Configures the pipeline to process the incoming response content as an object with + * the given {@link JavaType}. + */ + public ResultStage> asObject(final JavaType javaType) { + return consume((response, entityDetails, context) -> + JsonResponseConsumers.create(objectMapper, javaType)); + } + + /** + * Configures the pipeline to process the incoming response content as an object with + * the given {@link Class}. + */ + public ResultStage> asObject(final Class clazz) { + return consume((response, entityDetails, context) -> + JsonResponseConsumers.create(objectMapper, clazz)); + } + + /** + * Configures the pipeline to process the incoming response content as an object with + * the given {@link TypeReference}. + */ + public ResultStage> asObject(final TypeReference typeReference) { + return consume((response, entityDetails, context) -> + JsonResponseConsumers.create(objectMapper, typeReference)); + } + + /** + * Configures the pipeline to process the incoming response content as a {@link JsonNode} instance. + */ + public ResultStage> asJsonNode() { + return consume((response, entityDetails, context) -> + JsonResponseConsumers.create(objectMapper.getFactory())); + } + + /** + * Configures the pipeline to process the incoming response content as a sequence of objects + * with the given {@link JavaType}. + */ + public ResultStage asSequence( + final JavaType javaType, + final JsonConsumer responseValidator, + final Callback errorCallback, + final JsonResultSink resultSink) { + return consume((response, entityDetails, context) -> + JsonResponseConsumers.create(objectMapper, javaType, responseValidator, errorCallback, resultSink)); + } + + /** + * Configures the pipeline to process the incoming response content as a sequence of objects + * with the given {@link Class}. + */ + public ResultStage asSequence( + final Class clazz, + final JsonConsumer responseValidator, + final Callback errorCallback, + final JsonResultSink resultSink) { + return consume((response, entityDetails, context) -> + JsonResponseConsumers.create(objectMapper, clazz, responseValidator, errorCallback, resultSink)); + } + + /** + * Configures the pipeline to process the incoming response content as a sequence of objects + * with the given {@link TypeReference}. + */ + public ResultStage asSequence( + final TypeReference typeReference, + final JsonConsumer responseValidator, + final Callback errorCallback, + final JsonResultSink resultSink) { + return consume((response, entityDetails, context) -> + JsonResponseConsumers.create(objectMapper, typeReference, responseValidator, errorCallback, resultSink)); + } + + /** + * Configures the pipeline to process the incoming response content as a sequence of events. + */ + public ResultStage asEvents( + final JsonConsumer responseValidator, + final Callback errorCallback, + final JsonTokenEventHandler eventHandler) { + return consume((response, entityDetails, context) -> + JsonResponseConsumers.create(objectMapper.getFactory(), responseValidator, errorCallback, eventHandler)); + } + + } + + /** + * Exchange result signal stage. + */ + public static class ResultStage { + + private final AsyncRequestProducer requestProducer; + private final HandlerResolver> responseConsumerResolver; + + private ResultStage( + final AsyncRequestProducer requestProducer, + final HandlerResolver> responseConsumerResolver) { + this.requestProducer = requestProducer; + this.responseConsumerResolver = responseConsumerResolver; + } + + /** + * Configures the pipeline to signal completion of the message exchange by calling the given + * {@link FutureCallback} upon completion. + */ + public CompletionStage result(final FutureCallback resultCallback) { + return new CompletionStage<>(requestProducer, responseConsumerResolver, resultCallback); + } + + /** + * Configures the pipeline to signal completion of the message exchange by triggering the given + * {@link CompletableFuture} upon completion. + */ + public CompletionStage result(final CompletableFuture future) { + Args.notNull(future, "Future"); + return result(new FutureCallback() { + + @Override + public void completed(final T result) { + future.complete(result); + } + + @Override + public void failed(final Exception ex) { + future.completeExceptionally(ex); + } + + @Override + public void cancelled() { + future.cancel(true); + } + + }); + } + + } + + /** + * Exchange completion stage. + */ + public static class CompletionStage { + + private final AsyncRequestProducer requestProducer; + private final HandlerResolver> responseConsumerResolver; + private final FutureCallback resultCallback; + + private CompletionStage( + final AsyncRequestProducer requestProducer, + final HandlerResolver> responseConsumerResolver, + final FutureCallback resultCallback) { + this.requestProducer = requestProducer; + this.responseConsumerResolver = responseConsumerResolver; + this.resultCallback = resultCallback; + } + + /** + * Creates {@link AsyncClientExchangeHandler} implementing the defined message exchange pipeline. + */ + public AsyncClientExchangeHandler create() { + + return new AbstractClientExchangeHandler(requestProducer, resultCallback) { + + @Override + protected AsyncResponseConsumer supplyConsumer( + final HttpResponse response, + final EntityDetails entityDetails, + final HttpContext context) throws HttpException { + final AsyncResponseConsumer requestConsumer = responseConsumerResolver.resolve(response, entityDetails, context); + if (requestConsumer == null) { + throw new HttpException("Unable to process response"); + } + return requestConsumer; + } + + }; + + } + + } + +} diff --git a/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AsyncJsonServerPipeline.java b/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AsyncJsonServerPipeline.java new file mode 100644 index 000000000..963c70e6c --- /dev/null +++ b/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/http/AsyncJsonServerPipeline.java @@ -0,0 +1,473 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.jackson2.http; + +import java.io.IOException; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.hc.core5.function.Resolver; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.ExchangeHandler; +import org.apache.hc.core5.http.HandlerResolver; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.MethodNotAllowedException; +import org.apache.hc.core5.http.Validator; +import org.apache.hc.core5.http.impl.ServerSupport; +import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.AsyncRequestConsumer; +import org.apache.hc.core5.http.nio.AsyncResponseProducer; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; +import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; +import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer; +import org.apache.hc.core5.http.nio.support.AbstractServerExchangeHandler; +import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; +import org.apache.hc.core5.http.nio.support.BasicResponseProducer; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.jackson2.JsonConsumer; +import org.apache.hc.core5.jackson2.JsonResultSink; +import org.apache.hc.core5.jackson2.JsonTokenEventHandler; +import org.apache.hc.core5.util.Args; + +/** + * Server side execution pipeline assembler that supplies {@link AsyncServerExchangeHandler} instances + * with the defined message exchange pipeline optimized for JSON message exchanges. + *

+ * Please note that {@link AsyncServerExchangeHandler} are stateful and may not be used concurrently + * by multiple message exchanges or re-used for subsequent message exchanges. + * + * @since 5.5 + */ +public final class AsyncJsonServerPipeline { + + private final ObjectMapper objectMapper; + + private AsyncJsonServerPipeline(final ObjectMapper objectMapper) { + this.objectMapper = Args.notNull(objectMapper, "Object mapper"); + } + + public static AsyncJsonServerPipeline assemble(final ObjectMapper objectMapper) { + return new AsyncJsonServerPipeline(objectMapper); + } + + /** + * Configures the pipeline to process the incoming request message stream provided the request + * message passes the validation. + */ + public RequestContentStage request(final Validator requestValidator) { + return new RequestContentStage(requestValidator); + } + + /** + * Configures the pipeline to process the incoming request message stream provided the request + * method of the incoming message is allowed. + */ + public RequestContentStage request(final Method... allowedMethods) { + return new RequestContentStage(request -> { + final String method = request.getMethod(); + if (!ServerSupport.isMethodAllowed(method, allowedMethods)) { + throw new MethodNotAllowedException(method + " not allowed"); + } + }); + } + + /** + * Configures the pipeline to process the incoming request message stream. + */ + public RequestContentStage request() { + return new RequestContentStage(null); + } + + /** + * Request content processing stage. + */ + public class RequestContentStage { + + private final Validator requestValidator; + + private RequestContentStage(final Validator requestValidator) { + this.requestValidator = requestValidator; + } + + /** + * Resolves {@link AsyncRequestConsumer} to be used by the pipeline to process the incoming + * request message stream. + * + * @param request content representation. + */ + public ResponseStage consume( + final HandlerResolver> requestConsumerResolver) { + return new ResponseStage<>(requestValidator, requestConsumerResolver); + } + + /** + * Configures the pipeline to process the incoming request content as an object + * with the given {@link JavaType}. + */ + public ResponseStage> asObject(final JavaType javaType) { + return consume((request, entityDetails, context) -> + JsonRequestConsumers.create(objectMapper, javaType)); + } + + /** + * Configures the pipeline to process the incoming request content as an object + * with the given {@link Class}. + */ + public ResponseStage> asObject(final Class clazz) { + return consume((request, entityDetails, context) -> + JsonRequestConsumers.create(objectMapper, clazz)); + } + + /** + * Configures the pipeline to process the incoming request content as an object + * with the given {@link TypeReference}. + */ + public ResponseStage> asObject(final TypeReference typeReference) { + return consume((request, entityDetails, context) -> + JsonRequestConsumers.create(objectMapper, typeReference)); + } + + /** + * Configures the pipeline to process the incoming request content as a {@link JsonNode} instance. + */ + public ResponseStage> asJsonNode() { + return consume((request, entityDetails, context) -> + JsonRequestConsumers.create(objectMapper.getFactory())); + } + + /** + * Configures the pipeline to process the incoming request content as a sequence of objects + * with the given {@link JavaType}. + */ + public ResponseStage asSequence( + final JavaType javaType, + final JsonConsumer validator, + final JsonResultSink resultSink) { + return consume((request, entityDetails, context) -> + JsonRequestConsumers.create(objectMapper, javaType, validator, resultSink)); + } + + /** + * Configures the pipeline to process the incoming request content as a sequence of objects + * with the given {@link Class}. + */ + public ResponseStage asSequence( + final Class clazz, + final JsonConsumer validator, + final JsonResultSink resultSink) { + return consume((request, entityDetails, context) -> + JsonRequestConsumers.create(objectMapper, clazz, validator, resultSink)); + } + + /** + * Configures the pipeline to process the incoming request content as a sequence of objects + * with the given {@link TypeReference}. + */ + public ResponseStage asSequence( + final TypeReference typeReference, + final JsonConsumer validator, + final JsonResultSink resultSink) { + return consume((request, entityDetails, context) -> + JsonRequestConsumers.create(objectMapper, typeReference, validator, resultSink)); + } + + /** + * Configures the pipeline to process the incoming request content as a sequence of objects + * with the given {@link TypeReference}. + */ + public ResponseStage asEvents( + final JsonConsumer validator, + final JsonTokenEventHandler eventHandler) { + return consume((request, entityDetails, context) -> + JsonRequestConsumers.create(objectMapper.getFactory(), validator, eventHandler)); + } + + /** + * Configures the pipeline to ignore and discard the incoming request content. + */ + public ResponseStage> ignoreContent() { + return consume((request, entityDetails, context) -> + new BasicRequestConsumer<>(DiscardingEntityConsumer::new)); + } + + } + + /** + * Response message stage. + */ + public class ResponseStage { + + private final Validator requestValidator; + private final HandlerResolver> requestConsumerResolver; + + private ResponseStage( + final Validator requestValidator, + final HandlerResolver> requestConsumerResolver) { + this.requestValidator = requestValidator; + this.requestConsumerResolver = requestConsumerResolver; + } + + /** + * Configures the pipeline to produces an outgoing response message stream based on + * the incoming request message and content. + */ + public ResponseContentStage response() { + return new ResponseContentStage<>(requestValidator, requestConsumerResolver); + } + + } + + /** + * Response content generation stage. + */ + public class ResponseContentStage { + + private final Validator requestValidator; + private final HandlerResolver> requestConsumerResolver; + + private ResponseContentStage( + final Validator requestValidator, + final HandlerResolver> requestConsumerResolver) { + this.requestValidator = requestValidator; + this.requestConsumerResolver = requestConsumerResolver; + } + + /** + * Resolves {@link AsyncResponseProducer} to be used by the pipeline to generate the outgoing response + * message stream based on the given response message object. + * + * @param response content representation. + */ + public RequestHandlingStage produce( + final Resolver responseProducerResolver) { + return new RequestHandlingStage<>(requestValidator, requestConsumerResolver, responseProducerResolver); + } + + /** + * Resolves {@link AsyncEntityProducer} to be used by the pipeline to generate the outgoing response + * content stream based on the given response content object. + * + * @param response content representation. + */ + public RequestHandlingStage> produceContent( + final Resolver dataProducerResolver) { + return new RequestHandlingStage<>( + requestValidator, + requestConsumerResolver, + m -> + new BasicResponseProducer(m.head(), dataProducerResolver.resolve(m.body()))); + } + + /** + * Configures the pipeline to represent the response message content as an object + * with the given {@link Class}. + */ + public RequestHandlingStage> asObject(final Class clazz) { + return produce(m -> JsonResponseProducers.create(m.head(), m.body(), objectMapper)); + } + + /** + * Configures the pipeline to represent the response message content as an object. + */ + public RequestHandlingStage> asJsonNode() { + return produce(m -> JsonResponseProducers.create(m.head(), m.body(), objectMapper)); + } + + /** + * Configures the pipeline to represent the response message content as a sequence + * of objects. + */ + public RequestHandlingStage asSequence(final ObjectProducer objectProducer) { + return produce(r -> JsonResponseProducers.create(r, objectMapper, objectProducer)); + } + + } + + /** + * Request handling stage. + */ + public class RequestHandlingStage { + + private final Validator requestValidator; + private final HandlerResolver> requestConsumerResolver; + private final Resolver responseProducerResolver; + + private RequestHandlingStage( + final Validator requestValidator, + final HandlerResolver> requestConsumerResolver, + final Resolver responseProducerResolver) { + this.requestValidator = requestValidator; + this.requestConsumerResolver = requestConsumerResolver; + this.responseProducerResolver = responseProducerResolver; + } + + /** + * Configures the pipeline to resolve the exception to a response message stream. + */ + public ExceptionStage exception( + final Resolver exceptionMapper) { + return new ExceptionStage<>(requestValidator, requestConsumerResolver, responseProducerResolver, exceptionMapper); + } + + /** + * Configures the pipeline to resolve the exception to a response error message. The response status + * code will be determined by default based on the exception type. + */ + public ExceptionStage errorMessage( + final Resolver messageMapper) { + return exception(ex -> + new BasicResponseProducer( + new BasicHttpResponse(ServerSupport.toStatusCode(ex)), + messageMapper.resolve(ex), + ContentType.TEXT_PLAIN)); + } + + /** + * Configures the pipeline to handle the message exchange by generating a response object + * based on the properties of the request object. + */ + public CompletionStage handle(final ExchangeHandler exchangeHandler) { + return errorMessage(ServerSupport::toErrorMessage).handle(exchangeHandler); + } + + } + + /** + * Exception handling stage. + */ + public class ExceptionStage { + + private final Validator requestValidator; + private final HandlerResolver> requestConsumerResolver; + private final Resolver responseProducerResolver; + private final Resolver exceptionMapper; + + private ExceptionStage( + final Validator requestValidator, + final HandlerResolver> requestConsumerResolver, + final Resolver responseProducerResolver, + final Resolver exceptionMapper) { + this.requestValidator = requestValidator; + this.requestConsumerResolver = requestConsumerResolver; + this.responseProducerResolver = responseProducerResolver; + this.exceptionMapper = exceptionMapper; + } + + /** + * Configures the pipeline to handle the message exchange by generating a response object + * based on the properties of the request object. + */ + public CompletionStage handle(final ExchangeHandler exchangeHandler) { + return new CompletionStage<>(requestValidator, requestConsumerResolver, responseProducerResolver, exchangeHandler, exceptionMapper); + } + + } + + /** + * Exchange completion stage. + */ + public class CompletionStage { + + private final Validator requestValidator; + private final HandlerResolver> requestConsumerResolver; + private final Resolver responseProducerResolver; + private final ExchangeHandler exchangeHandler; + private final Resolver exceptionMapper; + + private CompletionStage( + final Validator requestValidator, + final HandlerResolver> requestConsumerResolver, + final Resolver responseProducerResolver, + final ExchangeHandler exchangeHandler, + final Resolver exceptionMapper) { + this.requestValidator = requestValidator; + this.requestConsumerResolver = requestConsumerResolver; + this.responseProducerResolver = responseProducerResolver; + this.exchangeHandler = exchangeHandler; + this.exceptionMapper = exceptionMapper; + } + + /** + * Creates {@link Supplier} of {@link AsyncServerExchangeHandler} implementing the defined message + * exchange pipeline. + */ + public Supplier supplier() { + return () -> new AbstractServerExchangeHandler() { + + @Override + protected AsyncRequestConsumer supplyConsumer( + final HttpRequest request, + final EntityDetails entityDetails, + final HttpContext context) throws HttpException { + if (requestValidator != null) { + requestValidator.validate(request); + } + final AsyncRequestConsumer requestConsumer = requestConsumerResolver.resolve(request, entityDetails, context); + if (requestConsumer == null) { + throw new HttpException("Unable to process request"); + } + return requestConsumer; + } + + @Override + protected void handle( + final I requestMessage, + final AsyncServerRequestHandler.ResponseTrigger responseTrigger, + final HttpContext context) throws HttpException, IOException { + final O responseMessage = exchangeHandler.handle(requestMessage, context); + if (responseMessage == null) { + throw new HttpException("Unable to handle request"); + } + final AsyncResponseProducer responseProducer = responseProducerResolver.resolve(responseMessage); + if (responseProducer == null) { + throw new HttpException("Unable to produce response"); + } + responseTrigger.submitResponse(responseProducer, context); + } + + @Override + protected AsyncResponseProducer handleError(final Exception ex) { + final AsyncResponseProducer responseProducer = exceptionMapper != null ? exceptionMapper.resolve(ex) : null; + return responseProducer != null ? responseProducer : super.handleError(ex); + } + + }; + } + + } + +} \ No newline at end of file diff --git a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonEntityConsumersTest.java b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonEntityConsumersTest.java index 2d237961e..75085d481 100644 --- a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonEntityConsumersTest.java +++ b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonEntityConsumersTest.java @@ -48,9 +48,9 @@ import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.UnsupportedMediaTypeException; import org.apache.hc.core5.http.impl.BasicEntityDetails; import org.apache.hc.core5.http.message.BasicHeader; -import org.apache.hc.core5.jackson2.JsonContentException; import org.apache.hc.core5.jackson2.JsonResultSink; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -119,8 +119,8 @@ void testWrongContentTypeThrowsException() throws Exception { final JsonNodeEntityConsumer entityConsumer = new JsonNodeEntityConsumer(factory); Assertions.assertThatThrownBy(() -> entityConsumer.streamStart( new BasicEntityDetails(-1, ContentType.TEXT_PLAIN), - null)).isInstanceOf(JsonContentException.class) - .hasMessage("Unexpected content type: text/plain"); + null)).isInstanceOf(UnsupportedMediaTypeException.class) + .hasMessage("Unsupported media type: text/plain"); } @Test diff --git a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonRequestConsumersTest.java b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonRequestConsumersTest.java index dc1a36ef3..7666444f5 100644 --- a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonRequestConsumersTest.java +++ b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonRequestConsumersTest.java @@ -40,12 +40,12 @@ import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.UnsupportedMediaTypeException; import org.apache.hc.core5.http.impl.BasicEntityDetails; import org.apache.hc.core5.http.message.BasicHttpRequest; import org.apache.hc.core5.http.nio.AsyncRequestConsumer; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.support.BasicRequestBuilder; -import org.apache.hc.core5.jackson2.JsonContentException; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -131,7 +131,8 @@ public void cancelled() { requestConsumer.consume(ByteBuffer.wrap("This is just plain text".getBytes(StandardCharsets.UTF_8))); requestConsumer.streamEnd(null); - Assertions.assertThat(resultRef.get()).isNotNull().isInstanceOf(JsonContentException.class).hasMessage("Unexpected content type: text/plain"); + Assertions.assertThat(resultRef.get()).isNotNull().isInstanceOf(UnsupportedMediaTypeException.class) + .hasMessage("Unsupported media type: text/plain"); } @Test @@ -226,7 +227,8 @@ public void cancelled() { Assertions.assertThat(messageRef.get()).isSameAs(request); Assertions.assertThat(resultList).isEmpty(); - Assertions.assertThat(resultRef.get()).isNotNull().isInstanceOf(JsonContentException.class).hasMessage("Unexpected content type: text/plain"); + Assertions.assertThat(resultRef.get()).isNotNull().isInstanceOf(UnsupportedMediaTypeException.class) + .hasMessage("Unsupported media type: text/plain"); } } diff --git a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonResponseConsumersTest.java b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonResponseConsumersTest.java index 8a6a7a032..2c9397ed8 100644 --- a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonResponseConsumersTest.java +++ b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/JsonResponseConsumersTest.java @@ -44,13 +44,13 @@ import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.UnsupportedMediaTypeException; import org.apache.hc.core5.http.impl.BasicEntityDetails; import org.apache.hc.core5.http.message.BasicHttpResponse; import org.apache.hc.core5.http.nio.AsyncResponseConsumer; import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.support.BasicResponseBuilder; -import org.apache.hc.core5.jackson2.JsonContentException; import org.apache.hc.core5.jackson2.JsonTokenConsumer; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -144,7 +144,8 @@ public void cancelled() { responseConsumer.consume(ByteBuffer.wrap("This is just plain text".getBytes(StandardCharsets.UTF_8))); responseConsumer.streamEnd(null); - Assertions.assertThat(resultRef.get()).isNotNull().isInstanceOf(JsonContentException.class).hasMessage("Unexpected content type: text/plain"); + Assertions.assertThat(resultRef.get()).isNotNull().isInstanceOf(UnsupportedMediaTypeException.class) + .hasMessage("Unsupported media type: text/plain"); } @Test @@ -241,7 +242,8 @@ public void cancelled() { Assertions.assertThat(messageRef.get()).isSameAs(response); Assertions.assertThat(resultList).isEmpty(); - Assertions.assertThat(resultRef.get()).isNotNull().isInstanceOf(JsonContentException.class).hasMessage("Unexpected content type: text/plain"); + Assertions.assertThat(resultRef.get()).isNotNull().isInstanceOf(UnsupportedMediaTypeException.class) + .hasMessage("Unsupported media type: text/plain"); } @Test diff --git a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonNodeResponseExample.java b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonNodeResponseExample.java index 76debae17..f094d8697 100644 --- a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonNodeResponseExample.java +++ b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonNodeResponseExample.java @@ -27,23 +27,20 @@ package org.apache.hc.core5.jackson2.http.examles; import java.net.URI; -import java.util.concurrent.Future; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.impl.bootstrap.AsyncRequesterBootstrap; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; -import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; +import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.jackson2.http.JsonResponseConsumers; +import org.apache.hc.core5.jackson2.http.AsyncJsonClientPipeline; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.util.Timeout; @@ -67,35 +64,42 @@ public static void main(final String... args) throws Exception { final URI uri = URI.create("http://httpbin.org/get"); - final JsonFactory factory = new JsonFactory(); - final ObjectMapper objectMapper = new ObjectMapper(factory); - System.out.println("Executing GET " + uri); - final Future future = requester.execute( - AsyncRequestBuilder.get(uri) - .addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()) - .build(), - JsonResponseConsumers.create(objectMapper.getFactory()), - Timeout.ofMinutes(1), - new FutureCallback>() { - - @Override - public void completed(final Message message) { - System.out.println("Response status: " + message.head().getCode()); - System.out.println(message.body()); - } - @Override - public void failed(final Exception ex) { - ex.printStackTrace(System.out); - } - - @Override - public void cancelled() { - } + final ObjectMapper objectMapper = new ObjectMapper(); + final CountDownLatch latch = new CountDownLatch(1); + requester.execute( + AsyncJsonClientPipeline.assemble(objectMapper) + .request() + .get(uri) + .response() + .asJsonNode() + .result(new FutureCallback>() { + + @Override + public void completed(final Message m) { + System.out.println("Response status: " + m.head().getCode()); + System.out.println(m.body()); + latch.countDown(); + } + + @Override + public void failed(final Exception ex) { + ex.printStackTrace(System.out); + latch.countDown(); + } + + @Override + public void cancelled() { + latch.countDown(); + } + + }) + .create(), + Timeout.ofMinutes(1), + HttpCoreContext.create()); - }); - future.get(); + latch.await(); System.out.println("Shutting down I/O reactor"); requester.initiateShutdown(); diff --git a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonObjectRequestExample.java b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonObjectRequestExample.java index 4a421889d..6129a5eb7 100644 --- a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonObjectRequestExample.java +++ b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonObjectRequestExample.java @@ -27,7 +27,7 @@ package org.apache.hc.core5.jackson2.http.examles; import java.net.URI; -import java.util.concurrent.Future; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import com.fasterxml.jackson.core.JsonFactory; @@ -35,17 +35,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.Message; import org.apache.hc.core5.http.impl.bootstrap.AsyncRequesterBootstrap; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; import org.apache.hc.core5.http.message.BasicNameValuePair; -import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; +import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.jackson2.http.JsonObjectEntityProducer; -import org.apache.hc.core5.jackson2.http.JsonResponseConsumers; +import org.apache.hc.core5.jackson2.http.AsyncJsonClientPipeline; import org.apache.hc.core5.jackson2.http.RequestData; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.util.Timeout; @@ -75,35 +72,40 @@ public static void main(final String... args) throws Exception { final ObjectMapper objectMapper = new ObjectMapper(factory); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - final Future future = requester.execute( - AsyncRequestBuilder.post(uri) - .addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()) - .setEntity(new JsonObjectEntityProducer<>( - new BasicNameValuePair("name", "value"), - objectMapper)) - .build(), - JsonResponseConsumers.create(objectMapper, RequestData.class), + final CountDownLatch latch = new CountDownLatch(1); + requester.execute( + AsyncJsonClientPipeline.assemble(objectMapper) + .request() + .post(uri) + .asObject(new BasicNameValuePair("name", "value")) + .response() + .asObject(RequestData.class) + .result(new FutureCallback>() { + + @Override + public void completed(final Message m) { + System.out.println("Response status: " + m.head().getCode()); + System.out.println(m.body()); + latch.countDown(); + } + + @Override + public void failed(final Exception ex) { + ex.printStackTrace(System.out); + latch.countDown(); + } + + @Override + public void cancelled() { + latch.countDown(); + } + + }) + .create(), Timeout.ofMinutes(1), - new FutureCallback>() { - - @Override - public void completed(final Message message) { - System.out.println("Response status: " + message.head().getCode()); - System.out.println(message.body()); - } - - @Override - public void failed(final Exception ex) { - ex.printStackTrace(System.out); - } - - @Override - public void cancelled() { - } - - }); - future.get(); + HttpCoreContext.create()); + latch.await(); System.out.println("Shutting down I/O reactor"); requester.initiateShutdown(); } diff --git a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonSequenceRequestExample.java b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonSequenceRequestExample.java index 9cfc01d68..ffae26913 100644 --- a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonSequenceRequestExample.java +++ b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonSequenceRequestExample.java @@ -27,7 +27,7 @@ package org.apache.hc.core5.jackson2.http.examles; import java.net.URI; -import java.util.concurrent.Future; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import com.fasterxml.jackson.core.JsonFactory; @@ -42,10 +42,10 @@ import org.apache.hc.core5.http.impl.bootstrap.AsyncRequesterBootstrap; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; import org.apache.hc.core5.http.message.BasicNameValuePair; -import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; +import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.http.support.BasicRequestBuilder; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.jackson2.http.JsonResponseConsumers; -import org.apache.hc.core5.jackson2.http.JsonSequenceEntityProducer; +import org.apache.hc.core5.jackson2.http.AsyncJsonClientPipeline; import org.apache.hc.core5.jackson2.http.RequestData; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.util.Timeout; @@ -75,37 +75,47 @@ public static void main(final String... args) throws Exception { objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); System.out.println("Executing POST " + uri); - final Future future = requester.execute( - AsyncRequestBuilder.post(uri) - .addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()) - .setEntity(new JsonSequenceEntityProducer<>(objectMapper, channel -> { + + final CountDownLatch latch = new CountDownLatch(1); + requester.execute( + AsyncJsonClientPipeline.assemble(objectMapper) + .request(BasicRequestBuilder.post(uri) + .addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()) + .build()) + .asSequence(channel -> { channel.write(new BasicNameValuePair("name1", "value1")); channel.write(new BasicNameValuePair("name2", "value2")); channel.write(new BasicNameValuePair("name3", "value3")); channel.endStream(); - })) - .build(), - JsonResponseConsumers.create(objectMapper, RequestData.class), + }) + .response() + .asObject(RequestData.class) + .result(new FutureCallback>() { + + @Override + public void completed(final Message m) { + System.out.println("Response status: " + m.head().getCode()); + System.out.println(m.body()); + latch.countDown(); + } + + @Override + public void failed(final Exception ex) { + ex.printStackTrace(System.out); + latch.countDown(); + } + + @Override + public void cancelled() { + latch.countDown(); + } + + }) + .create(), Timeout.ofMinutes(1), - new FutureCallback>() { - - @Override - public void completed(final Message message) { - System.out.println("Response status: " + message.head().getCode()); - System.out.println(message.body()); - } - - @Override - public void failed(final Exception ex) { - ex.printStackTrace(System.out); - } - - @Override - public void cancelled() { - } + HttpCoreContext.create()); - }); - future.get(); + latch.await(); System.out.println("Shutting down I/O reactor"); requester.initiateShutdown(); diff --git a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonSequenceResponseExample.java b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonSequenceResponseExample.java index e8ed92b9e..443ae1283 100644 --- a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonSequenceResponseExample.java +++ b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonSequenceResponseExample.java @@ -27,20 +27,18 @@ package org.apache.hc.core5.jackson2.http.examles; import java.net.URI; -import java.util.concurrent.Future; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.impl.bootstrap.AsyncRequesterBootstrap; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; -import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; +import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.io.CloseMode; -import org.apache.hc.core5.jackson2.http.JsonResponseConsumers; +import org.apache.hc.core5.jackson2.http.AsyncJsonClientPipeline; import org.apache.hc.core5.jackson2.http.RequestData; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.util.Timeout; @@ -69,35 +67,43 @@ public static void main(final String... args) throws Exception { final ObjectMapper objectMapper = new ObjectMapper(factory); System.out.println("Executing GET " + uri); - final Future future = requester.execute( - AsyncRequestBuilder.get(uri) - .addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()) - .build(), - JsonResponseConsumers.create( - objectMapper, - RequestData.class, - messageHead -> System.out.println("Response status: " + messageHead.getCode()), - error -> System.out.println("Error: " + error), - requestData -> System.out.println(requestData)), - Timeout.ofMinutes(1), - new FutureCallback() { - - @Override - public void completed(final Long count) { - System.out.println("Objects received: " + count); - } - @Override - public void failed(final Exception ex) { - ex.printStackTrace(System.out); - } - - @Override - public void cancelled() { - } + final CountDownLatch latch = new CountDownLatch(1); + requester.execute( + AsyncJsonClientPipeline.assemble(objectMapper) + .request() + .get(uri) + .response() + .asSequence( + RequestData.class, + r -> System.out.println("Response status: " + r.getCode()), + error -> System.out.println("Error: " + error), + requestData -> System.out.println(requestData)) + .result(new FutureCallback() { + + @Override + public void completed(final Long count) { + System.out.println("Objects received: " + count); + latch.countDown(); + } + + @Override + public void failed(final Exception ex) { + ex.printStackTrace(System.out); + latch.countDown(); + } + + @Override + public void cancelled() { + latch.countDown(); + } + + }) + .create(), + Timeout.ofMinutes(1), + HttpCoreContext.create()); - }); - future.get(); + latch.await(); System.out.println("Shutting down I/O reactor"); requester.initiateShutdown(); diff --git a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonServerExample.java b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonServerExample.java new file mode 100644 index 000000000..5df68a411 --- /dev/null +++ b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonServerExample.java @@ -0,0 +1,137 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.jackson2.http.examles; + +import java.net.InetSocketAddress; +import java.net.URISyntaxException; +import java.util.Objects; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.EndpointDetails; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.ProtocolException; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.impl.bootstrap.AsyncServerBootstrap; +import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; +import org.apache.hc.core5.http.impl.routing.RequestRouter; +import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; +import org.apache.hc.core5.http.protocol.HttpCoreContext; +import org.apache.hc.core5.http.protocol.HttpDateGenerator; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.jackson2.http.AsyncJsonServerPipeline; +import org.apache.hc.core5.jackson2.http.RequestData; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.reactor.ListenerEndpoint; +import org.apache.hc.core5.util.TimeValue; + +/** + * Example of asynchronous embedded JSON server. + */ +public class JsonServerExample { + + /** + * Example command line args: {@code 8080} + */ + public static void main(final String[] args) throws Exception { + int port = 8080; + if (args.length >= 1) { + port = Integer.parseInt(args[1]); + } + + final IOReactorConfig config = IOReactorConfig.custom() + .setSoTimeout(15, TimeUnit.SECONDS) + .setTcpNoDelay(true) + .build(); + + final ObjectMapper objectMapper = new ObjectMapper(); + + final Supplier exchangeHandlerSupplier = AsyncJsonServerPipeline.assemble(objectMapper) + // Read GET / HEAD requests by consuming content stream as JSON nodes + .request(Method.GET, Method.HEAD, Method.POST, Method.PUT, Method.PATCH) + .asJsonNode() + // Write out responses by streaming out content of JSON object + .response() + .asObject(RequestData.class) + // Map exceptions to a response message + .errorMessage(Throwable::getMessage) + // Generate a response to a request + .handle((m, context) -> { + final HttpRequest request = m.head(); + final RequestData rd = new RequestData(); + try { + rd.setUrl(request.getUri()); + } catch (final URISyntaxException ex) { + throw new ProtocolException("Invalid request URI"); + } + rd.generateHeaders(request.getHeaders()); + rd.setJson(m.body()); + rd.setData(Objects.toString(m.error())); + + final HttpCoreContext coreContext = HttpCoreContext.cast(context); + final EndpointDetails endpointDetails = coreContext.getEndpointDetails(); + + final InetSocketAddress remoteAddress = (InetSocketAddress) endpointDetails.getRemoteAddress(); + rd.setOrigin(Objects.toString(remoteAddress.getAddress())); + + return Message.of(new BasicHttpResponse(HttpStatus.SC_OK), rd); + }) + .supplier(); + + final HttpAsyncServer server = AsyncServerBootstrap.bootstrap() + .setExceptionCallback(e -> e.printStackTrace()) + .setIOReactorConfig(config) + .setRequestRouter(RequestRouter.>builder() + .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", exchangeHandlerSupplier) + .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) + .build()) + .create(); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + println("HTTP server shutting down"); + server.close(CloseMode.GRACEFUL); + })); + + server.start(); + final Future future = server.listen(new InetSocketAddress(port), URIScheme.HTTP); + final ListenerEndpoint listenerEndpoint = future.get(); + println("Listening on " + listenerEndpoint.getAddress()); + server.awaitShutdown(TimeValue.MAX_VALUE); + } + + static void println(final String msg) { + System.out.println(HttpDateGenerator.INSTANCE.getCurrentDate() + " | " + msg); + } + +} \ No newline at end of file diff --git a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonTokenEventResponseExample.java b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonTokenEventResponseExample.java index 8bad326d4..d2cbbc103 100644 --- a/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonTokenEventResponseExample.java +++ b/httpcore5-jackson2/src/test/java/org/apache/hc/core5/jackson2/http/examles/JsonTokenEventResponseExample.java @@ -27,21 +27,19 @@ package org.apache.hc.core5.jackson2.http.examles; import java.net.URI; -import java.util.concurrent.Future; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.impl.bootstrap.AsyncRequesterBootstrap; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; -import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; +import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.jackson2.JsonTokenEventHandler; -import org.apache.hc.core5.jackson2.http.JsonResponseConsumers; +import org.apache.hc.core5.jackson2.http.AsyncJsonClientPipeline; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.util.Timeout; @@ -69,101 +67,108 @@ public static void main(final String... args) throws Exception { final JsonFactory factory = new JsonFactory(); final ObjectMapper objectMapper = new ObjectMapper(factory); - final Future future = requester.execute( - AsyncRequestBuilder.get(uri) - .addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()) - .build(), - JsonResponseConsumers.create( - objectMapper.getFactory(), - messageHead -> System.out.println("Response status: " + messageHead.getCode()), - error -> System.out.println("Error: " + error), - new JsonTokenEventHandler() { + final CountDownLatch latch = new CountDownLatch(1); + requester.execute( + AsyncJsonClientPipeline.assemble(objectMapper) + .request() + .get(uri) + .response() + .asEvents( + response -> System.out.println("Response status: " + response.getCode()), + error -> System.out.println("Error: " + error), + new JsonTokenEventHandler() { + + @Override + public void objectStart() { + System.out.print("object start/"); + } + + @Override + public void objectEnd() { + System.out.print("object end/"); + } + + @Override + public void arrayStart() { + System.out.print("array start/"); + } + + @Override + public void arrayEnd() { + System.out.print("array end/"); + } + + @Override + public void field(final String name) { + System.out.print(name + "="); + } + + @Override + public void embeddedObject(final Object object) { + System.out.print(object + "/"); + } + + @Override + public void value(final String value) { + System.out.print("\"" + value + "\"/"); + } + + @Override + public void value(final int value) { + System.out.print(value + "/"); + } + + @Override + public void value(final long value) { + System.out.print(value + "/"); + } + + @Override + public void value(final double value) { + System.out.print(value + "/"); + } + + @Override + public void value(final boolean value) { + System.out.print(value + "/"); + } + + @Override + public void valueNull() { + System.out.print("null/"); + } + + @Override + public void endOfStream() { + System.out.println("stream end/"); + } + + }) + .result(new FutureCallback() { @Override - public void objectStart() { - System.out.print("object start/"); + public void completed(final Void input) { + System.out.println("Object received"); + latch.countDown(); } @Override - public void objectEnd() { - System.out.print("object end/"); + public void failed(final Exception ex) { + ex.printStackTrace(System.out); + latch.countDown(); } @Override - public void arrayStart() { - System.out.print("array start/"); + public void cancelled() { + latch.countDown(); } - @Override - public void arrayEnd() { - System.out.print("array end/"); - } - - @Override - public void field(final String name) { - System.out.print(name + "="); - } - - @Override - public void embeddedObject(final Object object) { - System.out.print(object + "/"); - } - - @Override - public void value(final String value) { - System.out.print("\"" + value + "\"/"); - } - - @Override - public void value(final int value) { - System.out.print(value + "/"); - } - - @Override - public void value(final long value) { - System.out.print(value + "/"); - } - - @Override - public void value(final double value) { - System.out.print(value + "/"); - } - - @Override - public void value(final boolean value) { - System.out.print(value + "/"); - } - - @Override - public void valueNull() { - System.out.print("null/"); - } - - @Override - public void endOfStream() { - System.out.println("stream end/"); - } - - }), + }) + .create(), Timeout.ofMinutes(1), - new FutureCallback() { - - @Override - public void completed(final Void input) { - System.out.println("Object received"); - } - - @Override - public void failed(final Exception ex) { - ex.printStackTrace(System.out); - } - - @Override - public void cancelled() { - } + HttpCoreContext.create()); - }); - future.get(); + latch.await(); System.out.println("Shutting down I/O reactor"); requester.initiateShutdown(); diff --git a/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/JsonContentException.java b/httpcore5/src/main/java/org/apache/hc/core5/http/ExchangeHandler.java similarity index 73% rename from httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/JsonContentException.java rename to httpcore5/src/main/java/org/apache/hc/core5/http/ExchangeHandler.java index b1a875acd..0194a9aec 100644 --- a/httpcore5-jackson2/src/main/java/org/apache/hc/core5/jackson2/JsonContentException.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/ExchangeHandler.java @@ -24,23 +24,21 @@ * . * */ -package org.apache.hc.core5.jackson2; -import com.fasterxml.jackson.core.JsonProcessingException; +package org.apache.hc.core5.http; + +import org.apache.hc.core5.http.protocol.HttpContext; /** - * Signals a failure processing a malformed or unexpected JSON message + * Represents a generic request / response message exchange. * + * @param request representation. + * @param response representation. * @since 5.5 */ -public class JsonContentException extends JsonProcessingException { - - public JsonContentException(final String msg) { - super(msg); - } +@FunctionalInterface +public interface ExchangeHandler { - public JsonContentException(final String msg, final Throwable rootCause) { - super(msg, rootCause); - } + O handle(I requestObject, HttpContext context) throws HttpException; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/HandlerResolver.java b/httpcore5/src/main/java/org/apache/hc/core5/http/HandlerResolver.java new file mode 100644 index 000000000..3029816d7 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/HandlerResolver.java @@ -0,0 +1,44 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http; + +import org.apache.hc.core5.http.protocol.HttpContext; + +/** + * Represents a generic request / response message exchange. + * + * @param message representation. + * @param handler type. + * @since 5.5 + */ +@FunctionalInterface +public interface HandlerResolver { + + H resolve(T message, EntityDetails entityDetails, HttpContext context) throws HttpException; + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/MethodNotAllowedException.java b/httpcore5/src/main/java/org/apache/hc/core5/http/MethodNotAllowedException.java new file mode 100644 index 000000000..80e4cde1c --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/MethodNotAllowedException.java @@ -0,0 +1,56 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http; + +/** + * Signals that an HTTP method is not allowed. + * + * @since 5.5 + */ +public class MethodNotAllowedException extends ProtocolException { + + /** + * Creates a new MethodNotAllowedException with the specified detail message. + * + * @param message The exception detail message + */ + public MethodNotAllowedException(final String message) { + super(message); + } + + /** + * Creates a new MethodNotAllowedException with the specified detail message and cause. + * + * @param message the exception detail message + * @param cause the {@code Throwable} that caused this exception, or {@code null} + * if the cause is unavailable, unknown, or not a {@code Throwable} + */ + public MethodNotAllowedException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/UnsupportedMediaTypeException.java b/httpcore5/src/main/java/org/apache/hc/core5/http/UnsupportedMediaTypeException.java new file mode 100644 index 000000000..63b33b16a --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/UnsupportedMediaTypeException.java @@ -0,0 +1,62 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http; + +/** + * Signals an unsupported media type. + * + * @since 5.5 + */ +public class UnsupportedMediaTypeException extends ProtocolException { + + /** + * Creates an exception without a detail message. + */ + public UnsupportedMediaTypeException() { + super(); + } + + /** + * Creates an exception with a detail message for the given ContentType. + * + * @param contentType The unsupported ContentType. + */ + public UnsupportedMediaTypeException(final ContentType contentType) { + super(contentType != null ? "Unsupported media type: " + contentType.getMimeType() : "Unsupported media type"); + } + + /** + * Creates an exception with the specified detail message. + * + * @param message The exception detail message + */ + public UnsupportedMediaTypeException(final String message) { + super(message); + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/Validator.java b/httpcore5/src/main/java/org/apache/hc/core5/http/Validator.java new file mode 100644 index 000000000..b3c916791 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/Validator.java @@ -0,0 +1,41 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http; + +/** + * Represents a generic request / response message exchange. + * + * @param message head type. + * @since 5.5 + */ +@FunctionalInterface +public interface Validator { + + void validate(H messageHead) throws HttpException; + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/ServerSupport.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/ServerSupport.java index 137923e4d..65883ce34 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/ServerSupport.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/ServerSupport.java @@ -28,12 +28,17 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.LengthRequiredException; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.MethodNotAllowedException; import org.apache.hc.core5.http.MethodNotSupportedException; import org.apache.hc.core5.http.MisdirectedRequestException; import org.apache.hc.core5.http.NotImplementedException; +import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.RequestHeaderFieldsTooLargeException; import org.apache.hc.core5.http.UnsupportedHttpVersionException; +import org.apache.hc.core5.http.UnsupportedMediaTypeException; /** * HTTP Server support methods. @@ -54,12 +59,20 @@ public static int toStatusCode(final Exception ex) { code = HttpStatus.SC_NOT_IMPLEMENTED; } else if (ex instanceof UnsupportedHttpVersionException) { code = HttpStatus.SC_HTTP_VERSION_NOT_SUPPORTED; + } else if (ex instanceof MethodNotAllowedException) { + code = HttpStatus.SC_METHOD_NOT_ALLOWED; + } else if (ex instanceof UnsupportedMediaTypeException) { + code = HttpStatus.SC_UNSUPPORTED_MEDIA_TYPE; } else if (ex instanceof NotImplementedException) { code = HttpStatus.SC_NOT_IMPLEMENTED; } else if (ex instanceof RequestHeaderFieldsTooLargeException) { code = HttpStatus.SC_REQUEST_HEADER_FIELDS_TOO_LARGE; } else if (ex instanceof MisdirectedRequestException) { code = HttpStatus.SC_MISDIRECTED_REQUEST; + } else if (ex instanceof LengthRequiredException) { + code = HttpStatus.SC_LENGTH_REQUIRED; + } else if (ex instanceof ParseException) { + code = HttpStatus.SC_BAD_REQUEST; } else if (ex instanceof ProtocolException) { code = HttpStatus.SC_BAD_REQUEST; } else { @@ -68,5 +81,17 @@ public static int toStatusCode(final Exception ex) { return code; } + /** + * @since 5.5 + */ + public static boolean isMethodAllowed(final String method, final Method... allowedMethods) { + for (final Method allowedMethod : allowedMethods) { + if (allowedMethod.isSame(method)) { + return true; + } + } + return false; + } + } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncRequester.java b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncRequester.java index c1f8ed13b..d359a64ff 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncRequester.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/impl/bootstrap/HttpAsyncRequester.java @@ -426,7 +426,7 @@ public final Future execute( final AsyncClientExchangeHandler exchangeHandler = new BasicClientExchangeHandler<>( requestProducer, responseConsumer, - new CompletingFutureContribution(future)); + new CompletingFutureContribution<>(future)); execute(target, exchangeHandler, pushHandlerFactory, timeout, context != null ? context : HttpCoreContext.create()); return future; } diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractClientExchangeHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractClientExchangeHandler.java new file mode 100644 index 000000000..5a34892d1 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractClientExchangeHandler.java @@ -0,0 +1,246 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http.nio.support; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.CapacityChannel; +import org.apache.hc.core5.http.nio.DataStreamChannel; +import org.apache.hc.core5.http.nio.RequestChannel; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.Args; + +/** + * Basic {@link AsyncClientExchangeHandler} implementation that makes use + * of {@link AsyncRequestProducer} to generate request message + * and {@link AsyncResponseConsumer} to process the response message returned by the server. + * + * @param The result type. + * @since 5.5 + */ +public abstract class AbstractClientExchangeHandler implements AsyncClientExchangeHandler { + + private final AsyncRequestProducer requestProducer; + private final AtomicReference> responseConsumerRef; + private final AtomicBoolean completed; + private final AtomicBoolean outputTerminated; + private final FutureCallback resultCallback; + + public AbstractClientExchangeHandler( + final AsyncRequestProducer requestProducer, + final FutureCallback resultCallback) { + this.requestProducer = Args.notNull(requestProducer, "Request producer"); + this.responseConsumerRef = new AtomicReference<>(); + this.completed = new AtomicBoolean(); + this.resultCallback = resultCallback; + this.outputTerminated = new AtomicBoolean(); + } + + @Override + public void produceRequest(final RequestChannel requestChannel, final HttpContext httpContext) throws HttpException, IOException { + requestProducer.sendRequest(requestChannel, httpContext); + } + + @Override + public int available() { + return requestProducer.available(); + } + + @Override + public void produce(final DataStreamChannel channel) throws IOException { + if (outputTerminated.get()) { + channel.endStream(); + return; + } + requestProducer.produce(channel); + } + + @Override + public void consumeInformation(final HttpResponse response, final HttpContext httpContext) throws HttpException, IOException { + } + + /** + * Triggered to supply a response consumer to process the incoming response. + * @param response the response message. + * @param entityDetails the request entity details. + * @param context the actual execution context. + * @return the request consumer. + * @throws HttpException in case of an HTTP protocol violation. + */ + protected abstract AsyncResponseConsumer supplyConsumer( + HttpResponse response, + EntityDetails entityDetails, + HttpContext context) throws HttpException; + + + @Override + public void consumeResponse(final HttpResponse response, final EntityDetails entityDetails, final HttpContext httpContext) throws HttpException, IOException { + if (response.getCode() >= HttpStatus.SC_CLIENT_ERROR) { + releaseRequestProducer(); + } + final AsyncResponseConsumer responseConsumer = supplyConsumer(response, entityDetails, httpContext); + if (responseConsumer == null) { + throw new HttpException("Unable to process response"); + } + responseConsumerRef.set(responseConsumer); + responseConsumerRef.get().consumeResponse(response, entityDetails, httpContext, new FutureCallback() { + + @Override + public void completed(final T result) { + completedInternal(result); + } + + @Override + public void failed(final Exception ex) { + failedInternal(ex); + } + + @Override + public void cancelled() { + cancelledInternal(); + } + + }); + } + + @Override + public void cancel() { + cancelledInternal(); + } + + @Override + public void updateCapacity(final CapacityChannel capacityChannel) throws IOException { + responseConsumerRef.get().updateCapacity(capacityChannel); + } + + @Override + public void consume(final ByteBuffer src) throws IOException { + responseConsumerRef.get().consume(src); + } + + @Override + public void streamEnd(final List trailers) throws HttpException, IOException { + responseConsumerRef.get().streamEnd(trailers); + } + + @Override + public void failed(final Exception cause) { + try { + final AsyncResponseConsumer responseConsumer = responseConsumerRef.getAndSet(null); + if (responseConsumer != null) { + try { + responseConsumer.failed(cause); + } finally { + responseConsumer.releaseResources(); + } + } + if (outputTerminated.compareAndSet(false, true)) { + requestProducer.failed(cause); + requestProducer.releaseResources(); + } + } finally { + failedInternal(cause); + } + } + + private void completedInternal(final T result) { + if (completed.compareAndSet(false, true)) { + try { + if (resultCallback != null) { + resultCallback.completed(result); + } + } finally { + releaseResourcesInternal(); + } + } + } + + private void failedInternal(final Exception ex) { + if (completed.compareAndSet(false, true)) { + try { + if (resultCallback != null) { + resultCallback.failed(ex); + } + } finally { + releaseResourcesInternal(); + } + } + } + + private void cancelledInternal() { + if (completed.compareAndSet(false, true)) { + try { + if (resultCallback != null) { + resultCallback.cancelled(); + } + } finally { + releaseResourcesInternal(); + } + } + } + + private void releaseResponseConsumer() { + final AsyncResponseConsumer responseConsumer = responseConsumerRef.getAndSet(null); + if (responseConsumer != null) { + responseConsumer.releaseResources(); + } + } + + private void releaseRequestProducer() { + if (outputTerminated.compareAndSet(false, true)) { + requestProducer.releaseResources(); + } + } + + private void releaseResourcesInternal() { + releaseRequestProducer(); + releaseResponseConsumer(); + } + + @Override + public void releaseResources() { + // Note even though the message exchange has been fully + // completed on the transport level, the response + // consumer may still be busy consuming and digesting + // the response message + releaseRequestProducer(); + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractServerExchangeHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractServerExchangeHandler.java index d33cfc6db..032d7ed23 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractServerExchangeHandler.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AbstractServerExchangeHandler.java @@ -32,12 +32,13 @@ import java.util.concurrent.atomic.AtomicReference; import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.impl.ServerSupport; import org.apache.hc.core5.http.nio.AsyncPushProducer; import org.apache.hc.core5.http.nio.AsyncRequestConsumer; import org.apache.hc.core5.http.nio.AsyncResponseProducer; @@ -104,7 +105,7 @@ public final void handleRequest( final AsyncRequestConsumer requestConsumer = supplyConsumer(request, entityDetails, context); if (requestConsumer == null) { - throw new HttpException("Unable to handle request"); + throw new HttpException("Unable to process request"); } requestConsumerRef.set(requestConsumer); final AsyncServerRequestHandler.ResponseTrigger responseTrigger = new AsyncServerRequestHandler.ResponseTrigger() { @@ -136,22 +137,20 @@ public String toString() { }; requestConsumer.consumeRequest(request, entityDetails, context, new FutureCallback() { + void triggerResponse(final AsyncResponseProducer errorProducer) { + try { + responseTrigger.submitResponse(errorProducer, context); + } catch (final HttpException | IOException ex) { + failedInternal(ex); + } + } + @Override public void completed(final T result) { try { handle(result, responseTrigger, context); - } catch (final HttpException ex) { - try { - responseTrigger.submitResponse( - AsyncResponseBuilder.create(HttpStatus.SC_INTERNAL_SERVER_ERROR) - .setEntity(ex.getMessage()) - .build(), - context); - } catch (final HttpException | IOException ex2) { - failedInternal(ex2); - } - } catch (final IOException ex) { - failedInternal(ex); + } catch (final HttpException | IOException ex) { + triggerResponse(handleError(ex)); } finally { releaseRequestConsumer(); } @@ -159,7 +158,11 @@ public void completed(final T result) { @Override public void failed(final Exception ex) { - failedInternal(ex); + if (ex instanceof HttpException || ex instanceof IOException) { + triggerResponse(handleError(ex)); + } else { + failedInternal(ex); + } } @Override @@ -205,6 +208,13 @@ public final void produce(final DataStreamChannel channel) throws IOException { dataProducer.produce(channel); } + protected AsyncResponseProducer handleError(final Exception ex) { + return new BasicResponseProducer( + ServerSupport.toStatusCode(ex), + ServerSupport.toErrorMessage(ex), + ContentType.TEXT_PLAIN); + } + @Override public final void failed(final Exception cause) { failedInternal(cause); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncClientPipeline.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncClientPipeline.java new file mode 100644 index 000000000..5dd77e524 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncClientPipeline.java @@ -0,0 +1,392 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http.nio.support; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; + +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.function.Resolver; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HandlerResolver; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.UnsupportedMediaTypeException; +import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; +import org.apache.hc.core5.http.nio.AsyncEntityConsumer; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; +import org.apache.hc.core5.http.nio.AsyncResponseConsumer; +import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer; +import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.support.BasicRequestBuilder; +import org.apache.hc.core5.util.Args; + +/** + * Client side execution pipeline assembler that creates {@link AsyncClientExchangeHandler} instances + * with the defined message exchange pipeline that triggers the given {@link FutureCallback} + * or {@link CompletableFuture} upon completion. + *

+ * Please note that {@link AsyncClientExchangeHandler} are stateful and may not be used concurrently + * by multiple message exchanges or re-used for subsequent message exchanges. + * + * @since 5.5 + */ +public final class AsyncClientPipeline { + + public static AsyncClientPipeline assemble() { + return new AsyncClientPipeline(); + } + + public RequestStage request() { + return new RequestStage(); + } + + /** + * Configures the pipeline to produce an outgoing message stream with the given + * request head. + */ + public RequestContentStage request(final HttpRequest request) { + return new RequestContentStage(request); + } + + /** + * Request message stage. + */ + public static class RequestStage { + + private RequestStage() { + } + + /** + * Configures {@link AsyncRequestProducer} to be used by the pipeline to generate the outgoing + * request message stream. + */ + public ResponseStage produce(final AsyncRequestProducer requestProducer) { + return new ResponseStage(requestProducer); + } + + /** + * Configures the pipeline to produce an outgoing GET message stream. + */ + public ResponseStage get(final URI requestUri) { + return new ResponseStage(AsyncRequestBuilder.get(requestUri) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing GET message stream. + */ + public ResponseStage get(final HttpHost target, final String path) { + return new ResponseStage(AsyncRequestBuilder.get() + .setHttpHost(target) + .setPath(path) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing POST message stream. + */ + public RequestContentStage post(final URI requestUri) { + return new RequestContentStage(BasicRequestBuilder.post(requestUri) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing POST message stream. + */ + public RequestContentStage post(final HttpHost target, final String path) { + return new RequestContentStage(BasicRequestBuilder.post() + .setHttpHost(target) + .setPath(path) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing PUT message stream. + */ + public RequestContentStage put(final URI requestUri) { + return new RequestContentStage(BasicRequestBuilder.put(requestUri) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing PUT message stream. + */ + public RequestContentStage put(final HttpHost target, final String path) { + return new RequestContentStage(BasicRequestBuilder.put() + .setHttpHost(target) + .setPath(path) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing PATCH message stream. + */ + public RequestContentStage patch(final URI requestUri) { + return new RequestContentStage(BasicRequestBuilder.patch(requestUri) + .build()); + } + + /** + * Configures the pipeline to produce an outgoing PATCH message stream. + */ + public RequestContentStage patch(final HttpHost target, final String path) { + return new RequestContentStage(BasicRequestBuilder.patch() + .setHttpHost(target) + .setPath(path) + .build()); + } + + } + + /** + * Request content stage. + */ + public static class RequestContentStage { + + private final HttpRequest request; + + private RequestContentStage(final HttpRequest request) { + this.request = request; + } + + /** + * Configures {@link AsyncEntityProducer} to be used by the pipeline to generate the outgoing + * request content stream. + */ + public ResponseStage produceContent(final AsyncEntityProducer dataProducer) { + return new ResponseStage(new BasicRequestProducer(request, dataProducer)); + } + + /** + * Configures the pipeline to represent the outgoing message content as a String. + */ + public ResponseStage asString(final String s, final ContentType contentType) { + return produceContent(new StringAsyncEntityProducer(s, contentType)); + } + + /** + * Configures the pipeline to represent the outgoing message content as a byte array. + */ + public ResponseStage asByteArray(final byte[] bytes, final ContentType contentType) { + return produceContent(new BasicAsyncEntityProducer(bytes, contentType)); + } + + /** + * Configures the pipeline to represent the outgoing message without a content body. + */ + public ResponseStage noContent() { + return produceContent(null); + } + + } + + /** + * Response message stage. + */ + public static class ResponseStage { + + private final AsyncRequestProducer requestProducer; + + private ResponseStage(final AsyncRequestProducer requestProducer) { + this.requestProducer = requestProducer; + } + + /** + * Configures the pipeline to processes the incoming response message stream. + */ + public ResponseContentStage response() { + return new ResponseContentStage(requestProducer); + } + + } + + /** + * Response content generation stage. + */ + public static class ResponseContentStage { + + private final AsyncRequestProducer requestProducer; + + private ResponseContentStage(final AsyncRequestProducer requestProducer) { + this.requestProducer = requestProducer; + } + + /** + * Configures {@link AsyncResponseConsumer} to be used by the pipeline to process + * the incoming response message stream. + * + * @param response content representation. + */ + public ResultStage consume(final HandlerResolver> responseConsumerResolver) { + return new ResultStage<>(requestProducer, responseConsumerResolver); + } + + /** + * Configures {@link AsyncEntityConsumer} supplier to be used by the pipeline to process + * the incoming response content stream. + * + * @param response content representation. + */ + public ResultStage> consumeContent( + final Resolver>> dataConsumerResolver) { + return new ResultStage<>( + requestProducer, + (response, entityDetails, context) -> { + if (entityDetails == null) { + return new BasicResponseConsumer<>(DiscardingEntityConsumer::new); + } + final ContentType contentType = ContentType.parseLenient(entityDetails.getContentType()); + final Supplier> supplier = dataConsumerResolver.resolve(contentType); + if (supplier == null) { + throw new UnsupportedMediaTypeException(contentType); + } + return new BasicResponseConsumer<>(supplier); + }); + } + + /** + * Configures the pipeline to process the incoming response content as a String. + */ + public ResultStage> asString() { + return consumeContent(contentType -> StringAsyncEntityConsumer::new); + } + + /** + * Configures the pipeline to process the incoming response content as a byte array. + */ + public ResultStage> asByteArray() { + return consumeContent(contentType -> BasicAsyncEntityConsumer::new); + } + + } + + /** + * Exchange result signal stage. + */ + public static class ResultStage { + + private final AsyncRequestProducer requestProducer; + private final HandlerResolver> responseConsumerResolver; + + private ResultStage( + final AsyncRequestProducer requestProducer, + final HandlerResolver> responseConsumerResolver) { + this.requestProducer = requestProducer; + this.responseConsumerResolver = responseConsumerResolver; + } + + /** + * Configures the pipeline to signal completion of the message exchange by calling the given + * {@link FutureCallback} upon completion. + */ + public CompletionStage result(final FutureCallback resultCallback) { + return new CompletionStage<>(requestProducer, responseConsumerResolver, resultCallback); + } + + /** + * Configures the pipeline to signal completion of the message exchange by triggering the given + * {@link CompletableFuture} upon completion. + */ + public CompletionStage result(final CompletableFuture future) { + Args.notNull(future, "Future"); + return result(new FutureCallback() { + + @Override + public void completed(final T result) { + future.complete(result); + } + + @Override + public void failed(final Exception ex) { + future.completeExceptionally(ex); + } + + @Override + public void cancelled() { + future.cancel(true); + } + + }); + } + + } + + /** + * Exchange completion stage. + */ + public static class CompletionStage { + + private final AsyncRequestProducer requestProducer; + private final HandlerResolver> responseConsumerResolver; + private final FutureCallback resultCallback; + + private CompletionStage( + final AsyncRequestProducer requestProducer, + final HandlerResolver> responseConsumerResolver, + final FutureCallback resultCallback) { + this.requestProducer = requestProducer; + this.responseConsumerResolver = responseConsumerResolver; + this.resultCallback = resultCallback; + } + + /** + * Creates {@link AsyncClientExchangeHandler} implementing the defined message exchange pipeline. + */ + public AsyncClientExchangeHandler create() { + + return new AbstractClientExchangeHandler(requestProducer, resultCallback) { + + @Override + protected AsyncResponseConsumer supplyConsumer( + final HttpResponse response, + final EntityDetails entityDetails, + final HttpContext context) throws HttpException { + final AsyncResponseConsumer requestConsumer = responseConsumerResolver.resolve(response, entityDetails, context); + if (requestConsumer == null) { + throw new HttpException("Unable to process response"); + } + return requestConsumer; + } + + }; + + } + + } + +} diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncServerPipeline.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncServerPipeline.java new file mode 100644 index 000000000..9778d1504 --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/AsyncServerPipeline.java @@ -0,0 +1,406 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.core5.http.nio.support; + +import java.io.IOException; + +import org.apache.hc.core5.function.Resolver; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.ExchangeHandler; +import org.apache.hc.core5.http.HandlerResolver; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.MethodNotAllowedException; +import org.apache.hc.core5.http.UnsupportedMediaTypeException; +import org.apache.hc.core5.http.Validator; +import org.apache.hc.core5.http.impl.ServerSupport; +import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.nio.AsyncEntityConsumer; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.AsyncRequestConsumer; +import org.apache.hc.core5.http.nio.AsyncResponseProducer; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; +import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; +import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityProducer; +import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; +import org.apache.hc.core5.http.protocol.HttpContext; + +/** + * Server side execution pipeline assembler that supplies {@link AsyncServerExchangeHandler} instances + * with the defined message exchange pipeline. + *

+ * Please note that {@link AsyncServerExchangeHandler} are stateful and may not be used concurrently + * by multiple message exchanges or re-used for subsequent message exchanges. + * + * @since 5.5 + */ +public final class AsyncServerPipeline { + + public static AsyncServerPipeline assemble() { + return new AsyncServerPipeline(); + } + + /** + * Configures the pipeline to process the incoming request message stream provided the request + * message passes the validation. + */ + public RequestContentStage request(final Validator requestValidator) { + return new RequestContentStage(requestValidator); + } + + /** + * Configures the pipeline to process the incoming request message stream provided the request + * method of the incoming message is allowed. + */ + public RequestContentStage request(final Method... allowedMethods) { + return new RequestContentStage(request -> { + final String method = request.getMethod(); + if (!ServerSupport.isMethodAllowed(method, allowedMethods)) { + throw new MethodNotAllowedException(method + " not allowed"); + } + }); + } + + /** + * Configures the pipeline to process the incoming request message stream. + */ + public RequestContentStage request() { + return new RequestContentStage(null); + } + + /** + * Request content processing stage. + */ + public static class RequestContentStage { + + private final Validator requestValidator; + + private RequestContentStage(final Validator requestValidator) { + this.requestValidator = requestValidator; + } + + /** + * Resolves {@link AsyncRequestConsumer} to be used by the pipeline to process the incoming + * request message stream. + * + * @param request content representation. + */ + public ResponseStage consume( + final HandlerResolver> requestConsumerResolver) { + return new ResponseStage<>(requestValidator, requestConsumerResolver); + } + + /** + * Resolves {@link AsyncEntityConsumer} to be used by the pipeline to process the incoming + * request content stream. Resolver may return {@code null} if the media type is not supported. + * + * @param request content representation. + */ + public ResponseStage> consumeContent( + final Resolver>> dataConsumerResolver) { + return new ResponseStage<>( + requestValidator, + (request, entityDetails, context) -> { + if (entityDetails == null) { + return new BasicRequestConsumer<>(DiscardingEntityConsumer::new); + } + final ContentType contentType = ContentType.parseLenient(entityDetails.getContentType()); + final Supplier> supplier = dataConsumerResolver.resolve(contentType); + if (supplier == null) { + throw new UnsupportedMediaTypeException(contentType); + } + return new BasicRequestConsumer<>(supplier); + }); + } + + /** + * Configures the pipeline to process the incoming request content as a String. + */ + public ResponseStage> asString() { + return consumeContent(contentType -> StringAsyncEntityConsumer::new); + } + + /** + * Configures the pipeline to process the incoming request content as a byte array. + */ + public ResponseStage> asByteArray() { + return consumeContent(contentType -> BasicAsyncEntityConsumer::new); + } + + /** + * Configures the pipeline to ignore and discard the incoming request content. + */ + public ResponseStage> ignoreContent() { + return consumeContent(contentType -> DiscardingEntityConsumer::new); + } + + } + + /** + * Response message stage. + */ + public static class ResponseStage { + + private final Validator requestValidator; + private final HandlerResolver> requestConsumerResolver; + + private ResponseStage( + final Validator requestValidator, + final HandlerResolver> requestConsumerResolver) { + this.requestValidator = requestValidator; + this.requestConsumerResolver = requestConsumerResolver; + } + + /** + * Configures the pipeline to produces an outgoing response message stream based on + * the incoming request message and content. + */ + public ResponseContentStage response() { + return new ResponseContentStage<>(requestValidator, requestConsumerResolver); + } + + } + + /** + * Response content generation stage. + */ + public static class ResponseContentStage { + + private final Validator requestValidator; + private final HandlerResolver> requestConsumerResolver; + + private ResponseContentStage( + final Validator requestValidator, + final HandlerResolver> requestConsumerResolver) { + this.requestValidator = requestValidator; + this.requestConsumerResolver = requestConsumerResolver; + } + + /** + * Resolves {@link AsyncResponseProducer} to be used by the pipeline to generate the outgoing response + * message stream based on the given response message object. + * + * @param response content representation. + */ + public RequestHandlingStage produce( + final Resolver responseProducerResolver) { + return new RequestHandlingStage<>(requestValidator, requestConsumerResolver, responseProducerResolver); + } + + /** + * Resolves {@link AsyncEntityProducer} to be used by the pipeline to generate the outgoing response + * content stream based on the given response content object. + * + * @param response content representation. + */ + public RequestHandlingStage> produceContent( + final Resolver dataProducerResolver) { + return new RequestHandlingStage<>( + requestValidator, + requestConsumerResolver, + m -> + new BasicResponseProducer(m.head(), dataProducerResolver.resolve(m.body()))); + } + + /** + * Configures the pipeline to represent the response message content as a String. + */ + public RequestHandlingStage> asString(final ContentType contentType) { + return produceContent(s -> new StringAsyncEntityProducer(s, contentType)); + } + + /** + * Configures the pipeline to represent the response message content as byre array. + */ + public RequestHandlingStage> asByteArray(final ContentType contentType) { + return produceContent(bytes -> new BasicAsyncEntityProducer(bytes, contentType)); + } + + } + + /** + * Request handling stage. + */ + public static class RequestHandlingStage { + + private final Validator requestValidator; + private final HandlerResolver> requestConsumerResolver; + private final Resolver responseProducerResolver; + + private RequestHandlingStage( + final Validator requestValidator, + final HandlerResolver> requestConsumerResolver, + final Resolver responseProducerResolver) { + this.requestValidator = requestValidator; + this.requestConsumerResolver = requestConsumerResolver; + this.responseProducerResolver = responseProducerResolver; + } + + /** + * Configures the pipeline to resolve the exception to a response message stream. + */ + public ExceptionStage exception( + final Resolver exceptionMapper) { + return new ExceptionStage<>(requestValidator, requestConsumerResolver, responseProducerResolver, exceptionMapper); + } + + /** + * Configures the pipeline to resolve the exception to a response error message. The response status + * code will be determined by default based on the exception type. + */ + public ExceptionStage errorMessage( + final Resolver messageMapper) { + return exception(ex -> + new BasicResponseProducer( + new BasicHttpResponse(ServerSupport.toStatusCode(ex)), + messageMapper.resolve(ex), + ContentType.TEXT_PLAIN)); + } + + /** + * Configures the pipeline to handle the message exchange by generating a response object + * based on the properties of the request object. + */ + public CompletionStage handle(final ExchangeHandler exchangeHandler) { + return errorMessage(ServerSupport::toErrorMessage).handle(exchangeHandler); + } + + } + + /** + * Exception handling stage. + */ + public static class ExceptionStage { + + private final Validator requestValidator; + private final HandlerResolver> requestConsumerResolver; + private final Resolver responseProducerResolver; + private final Resolver exceptionMapper; + + private ExceptionStage( + final Validator requestValidator, + final HandlerResolver> requestConsumerResolver, + final Resolver responseProducerResolver, + final Resolver exceptionMapper) { + this.requestValidator = requestValidator; + this.requestConsumerResolver = requestConsumerResolver; + this.responseProducerResolver = responseProducerResolver; + this.exceptionMapper = exceptionMapper; + } + + /** + * Configures the pipeline to handle the message exchange by generating a response object + * based on the properties of the request object. + */ + public CompletionStage handle(final ExchangeHandler exchangeHandler) { + return new CompletionStage<>(requestValidator, requestConsumerResolver, responseProducerResolver, exchangeHandler, exceptionMapper); + } + + } + + /** + * Exchange completion stage. + */ + public static class CompletionStage { + + private final Validator requestValidator; + private final HandlerResolver> requestConsumerResolver; + private final Resolver responseProducerResolver; + private final ExchangeHandler exchangeHandler; + private final Resolver exceptionMapper; + + private CompletionStage( + final Validator requestValidator, + final HandlerResolver> requestConsumerResolver, + final Resolver responseProducerResolver, + final ExchangeHandler exchangeHandler, + final Resolver exceptionMapper) { + this.requestValidator = requestValidator; + this.requestConsumerResolver = requestConsumerResolver; + this.responseProducerResolver = responseProducerResolver; + this.exchangeHandler = exchangeHandler; + this.exceptionMapper = exceptionMapper; + } + + /** + * Creates {@link Supplier} of {@link AsyncServerExchangeHandler} implementing the defined message + * exchange pipeline. + */ + public Supplier supplier() { + return () -> new AbstractServerExchangeHandler() { + + @Override + protected AsyncRequestConsumer supplyConsumer( + final HttpRequest request, + final EntityDetails entityDetails, + final HttpContext context) throws HttpException { + if (requestValidator != null) { + requestValidator.validate(request); + } + final AsyncRequestConsumer requestConsumer = requestConsumerResolver.resolve(request, entityDetails, context); + if (requestConsumer == null) { + throw new HttpException("Unable to process request"); + } + return requestConsumer; + } + + @Override + protected void handle( + final I requestMessage, + final AsyncServerRequestHandler.ResponseTrigger responseTrigger, + final HttpContext context) throws HttpException, IOException { + final O responseMessage = exchangeHandler.handle(requestMessage, context); + if (responseMessage == null) { + throw new HttpException("Unable to handle request"); + } + final AsyncResponseProducer responseProducer = responseProducerResolver.resolve(responseMessage); + if (responseProducer == null) { + throw new HttpException("Unable to produce response"); + } + responseTrigger.submitResponse(responseProducer, context); + } + + @Override + protected AsyncResponseProducer handleError(final Exception ex) { + final AsyncResponseProducer responseProducer = exceptionMapper != null ? exceptionMapper.resolve(ex) : null; + return responseProducer != null ? responseProducer : super.handleError(ex); + } + + }; + } + + } + +} \ No newline at end of file diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/BasicClientExchangeHandler.java b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/BasicClientExchangeHandler.java index c74b7d319..bcd8587be 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/BasicClientExchangeHandler.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/nio/support/BasicClientExchangeHandler.java @@ -26,25 +26,14 @@ */ package org.apache.hc.core5.http.nio.support; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.EntityDetails; -import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpResponse; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler; import org.apache.hc.core5.http.nio.AsyncRequestProducer; import org.apache.hc.core5.http.nio.AsyncResponseConsumer; -import org.apache.hc.core5.http.nio.CapacityChannel; -import org.apache.hc.core5.http.nio.DataStreamChannel; -import org.apache.hc.core5.http.nio.RequestChannel; import org.apache.hc.core5.http.protocol.HttpContext; -import org.apache.hc.core5.util.Args; /** * Basic {@link AsyncClientExchangeHandler} implementation that makes use @@ -54,172 +43,24 @@ * @param The result type. * @since 5.0 */ -public final class BasicClientExchangeHandler implements AsyncClientExchangeHandler { +public final class BasicClientExchangeHandler extends AbstractClientExchangeHandler { - private final AsyncRequestProducer requestProducer; private final AsyncResponseConsumer responseConsumer; - private final AtomicBoolean completed; - private final AtomicBoolean outputTerminated; - private final AtomicBoolean inputTerminated; - private final FutureCallback resultCallback; public BasicClientExchangeHandler( final AsyncRequestProducer requestProducer, final AsyncResponseConsumer responseConsumer, final FutureCallback resultCallback) { - this.requestProducer = Args.notNull(requestProducer, "Request producer"); - this.responseConsumer = Args.notNull(responseConsumer, "Response consumer"); - this.completed = new AtomicBoolean(); - this.resultCallback = resultCallback; - this.outputTerminated = new AtomicBoolean(); - this.inputTerminated = new AtomicBoolean(); - } - - @Override - public void produceRequest(final RequestChannel requestChannel, final HttpContext httpContext) throws HttpException, IOException { - requestProducer.sendRequest(requestChannel, httpContext); - } - - @Override - public int available() { - return requestProducer.available(); - } - - @Override - public void produce(final DataStreamChannel channel) throws IOException { - if (outputTerminated.get()) { - channel.endStream(); - return; - } - requestProducer.produce(channel); - } - - @Override - public void consumeInformation(final HttpResponse response, final HttpContext httpContext) throws HttpException, IOException { - responseConsumer.informationResponse(response, httpContext); - } - - @Override - public void consumeResponse(final HttpResponse response, final EntityDetails entityDetails, final HttpContext httpContext) throws HttpException, IOException { - if (response.getCode() >= HttpStatus.SC_CLIENT_ERROR) { - releaseRequestProducer(); - } - responseConsumer.consumeResponse(response, entityDetails, httpContext, new FutureCallback() { - - @Override - public void completed(final T result) { - completedInternal(result); - } - - @Override - public void failed(final Exception ex) { - failedInternal(ex); - } - - @Override - public void cancelled() { - cancelledInternal(); - } - - }); - } - - @Override - public void cancel() { - cancelledInternal(); - } - - @Override - public void updateCapacity(final CapacityChannel capacityChannel) throws IOException { - responseConsumer.updateCapacity(capacityChannel); - } - - @Override - public void consume(final ByteBuffer src) throws IOException { - responseConsumer.consume(src); - } - - @Override - public void streamEnd(final List trailers) throws HttpException, IOException { - responseConsumer.streamEnd(trailers); - } - - @Override - public void failed(final Exception cause) { - try { - if (inputTerminated.compareAndSet(false, true)) { - responseConsumer.failed(cause); - responseConsumer.releaseResources(); - } - if (outputTerminated.compareAndSet(false, true)) { - requestProducer.failed(cause); - requestProducer.releaseResources(); - } - } finally { - failedInternal(cause); - } - } - - private void completedInternal(final T result) { - if (completed.compareAndSet(false, true)) { - try { - if (resultCallback != null) { - resultCallback.completed(result); - } - } finally { - releaseResourcesInternal(); - } - } - } - - private void failedInternal(final Exception ex) { - if (completed.compareAndSet(false, true)) { - try { - if (resultCallback != null) { - resultCallback.failed(ex); - } - } finally { - releaseResourcesInternal(); - } - } - } - - private void cancelledInternal() { - if (completed.compareAndSet(false, true)) { - try { - if (resultCallback != null) { - resultCallback.cancelled(); - } - } finally { - releaseResourcesInternal(); - } - } - } - - private void releaseResponseConsumer() { - if (inputTerminated.compareAndSet(false, true)) { - responseConsumer.releaseResources(); - } - } - - private void releaseRequestProducer() { - if (outputTerminated.compareAndSet(false, true)) { - requestProducer.releaseResources(); - } - } - - private void releaseResourcesInternal() { - releaseRequestProducer(); - releaseResponseConsumer(); + super(requestProducer, resultCallback); + this.responseConsumer = responseConsumer; } @Override - public void releaseResources() { - // Note even though the message exchange has been fully - // completed on the transport level, the response - // consumer may still be busy consuming and digesting - // the response message - releaseRequestProducer(); + protected AsyncResponseConsumer supplyConsumer( + final HttpResponse response, + final EntityDetails entityDetails, + final HttpContext context) throws HttpException { + return responseConsumer; } } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncClientSNIExample.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncClientSNIExample.java index a5a8cb21e..7fd39e550 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncClientSNIExample.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncClientSNIExample.java @@ -44,9 +44,7 @@ import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; import org.apache.hc.core5.http.message.RequestLine; import org.apache.hc.core5.http.message.StatusLine; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.support.BasicRequestProducer; -import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.nio.support.AsyncClientPipeline; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.support.BasicRequestBuilder; import org.apache.hc.core5.io.CloseMode; @@ -104,47 +102,52 @@ public void onExchangeComplete(final HttpConnection connection, final boolean ke final HttpCoreContext context = HttpCoreContext.create(); final CountDownLatch latch = new CountDownLatch(1); - final HttpRequest request = BasicRequestBuilder.get() - .setPath("/") - .build(); + final String path = "/"; requester.execute( target, - new BasicRequestProducer(request, null), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - null, - Timeout.ofSeconds(5), - context, - new FutureCallback>() { - - @Override - public void completed(final Message message) { - final HttpResponse response = message.head(); - System.out.println(request.getRequestUri() + "->" + response.getCode()); - final SSLSession sslSession = context.getSSLSession(); - if (sslSession != null) { - try { - System.out.println("Peer: " + sslSession.getPeerPrincipal()); - System.out.println("TLS protocol: " + sslSession.getProtocol()); - System.out.println("TLS cipher suite: " + sslSession.getCipherSuite()); - } catch (final SSLPeerUnverifiedException ignore) { + AsyncClientPipeline.assemble() + .request(BasicRequestBuilder.get() + .setHttpHost(target) + .setPath(path) + .build()) + .noContent() + .response() + .asString() + .result(new FutureCallback>() { + + @Override + public void completed(final Message message) { + final HttpResponse response = message.head(); + System.out.println(path + "->" + response.getCode()); + final SSLSession sslSession = context.getSSLSession(); + if (sslSession != null) { + try { + System.out.println("Peer: " + sslSession.getPeerPrincipal()); + System.out.println("TLS protocol: " + sslSession.getProtocol()); + System.out.println("TLS cipher suite: " + sslSession.getCipherSuite()); + } catch (final SSLPeerUnverifiedException ignore) { + } + } + latch.countDown(); } - } - latch.countDown(); - } - @Override - public void failed(final Exception ex) { - System.out.println(request.getRequestUri() + "->" + ex); - latch.countDown(); - } + @Override + public void failed(final Exception ex) { + System.out.println(path + "->" + ex); + latch.countDown(); + } - @Override - public void cancelled() { - System.out.println(request.getRequestUri() + " cancelled"); - latch.countDown(); - } + @Override + public void cancelled() { + System.out.println(path + " cancelled"); + latch.countDown(); + } - }); + }) + .create(), + null, + Timeout.ofMinutes(1), + context); latch.await(); System.out.println("Shutting down I/O reactor"); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncFileServerExample.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncFileServerExample.java index 1ee1f75b6..4c60b977c 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncFileServerExample.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncFileServerExample.java @@ -27,31 +27,32 @@ package org.apache.hc.core5.http.examples; import java.io.File; -import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; +import java.util.Objects; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.EndpointDetails; -import org.apache.hc.core5.http.EntityDetails; -import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.Method; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http.impl.bootstrap.AsyncServerBootstrap; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; -import org.apache.hc.core5.http.nio.AsyncRequestConsumer; -import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; -import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; -import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer; -import org.apache.hc.core5.http.nio.support.AsyncResponseBuilder; -import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; -import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.http.impl.routing.RequestRouter; +import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; +import org.apache.hc.core5.http.nio.entity.FileEntityProducer; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; +import org.apache.hc.core5.http.nio.support.AsyncServerPipeline; +import org.apache.hc.core5.http.nio.support.BasicResponseProducer; import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.http.protocol.HttpDateGenerator; import org.apache.hc.core5.io.CloseMode; @@ -85,80 +86,66 @@ public static void main(final String[] args) throws Exception { .setTcpNoDelay(true) .build(); - final HttpAsyncServer server = AsyncServerBootstrap.bootstrap() - .setExceptionCallback(e -> e.printStackTrace()) - .setIOReactorConfig(config) - .register("*", new AsyncServerRequestHandler>() { - - @Override - public AsyncRequestConsumer> prepare( - final HttpRequest request, - final EntityDetails entityDetails, - final HttpContext context) throws HttpException { - return new BasicRequestConsumer<>(entityDetails != null ? new DiscardingEntityConsumer<>() : null); - } - - @Override - public void handle( - final Message message, - final ResponseTrigger responseTrigger, - final HttpContext localContext) throws HttpException, IOException { - final HttpCoreContext context = HttpCoreContext.cast(localContext); - final HttpRequest request = message.head(); - final URI requestUri; - try { - requestUri = request.getUri(); - } catch (final URISyntaxException ex) { - throw new ProtocolException(ex.getMessage(), ex); - } - final String path = requestUri.getPath(); - final File file = new File(docRoot, path); - if (!file.exists()) { - - final String msg = "File " + file.getPath() + " not found"; - println(msg); - responseTrigger.submitResponse( - AsyncResponseBuilder.create(HttpStatus.SC_NOT_FOUND) - .setEntity("

" + msg + "

", ContentType.TEXT_HTML) - .build(), - context); - - } else if (!file.canRead() || file.isDirectory()) { - - final String msg = "Cannot read file " + file.getPath(); - println(msg); - responseTrigger.submitResponse(AsyncResponseBuilder.create(HttpStatus.SC_FORBIDDEN) - .setEntity("

" + msg + "

", ContentType.TEXT_HTML) - .build(), - context); - + final Supplier exchangeHandlerSupplier = AsyncServerPipeline.assemble() + // Read GET / HEAD requests ignoring their content body + .request(Method.GET, Method.HEAD) + .ignoreContent() + // Write out responses by streaming out content of a file + .response() + .>produce(m -> { + if (m.error() == null) { + final File file = m.getBody(); + final ContentType contentType; + final String filename = TextUtils.toLowerCase(file.getName()); + if (filename.endsWith(".txt")) { + contentType = ContentType.TEXT_PLAIN; + } else if (filename.endsWith(".html") || filename.endsWith(".htm")) { + contentType = ContentType.TEXT_HTML; + } else if (filename.endsWith(".xml")) { + contentType = ContentType.TEXT_XML; } else { - - final ContentType contentType; - final String filename = TextUtils.toLowerCase(file.getName()); - if (filename.endsWith(".txt")) { - contentType = ContentType.TEXT_PLAIN; - } else if (filename.endsWith(".html") || filename.endsWith(".htm")) { - contentType = ContentType.TEXT_HTML; - } else if (filename.endsWith(".xml")) { - contentType = ContentType.TEXT_XML; - } else { - contentType = ContentType.DEFAULT_BINARY; - } - - final EndpointDetails endpoint = context.getEndpointDetails(); - - println(endpoint + " | serving file " + file.getPath()); - - responseTrigger.submitResponse( - AsyncResponseBuilder.create(HttpStatus.SC_OK) - .setEntity(AsyncEntityProducers.create(file, contentType)) - .build(), - context); + contentType = ContentType.DEFAULT_BINARY; } + return new BasicResponseProducer(new FileEntityProducer(file, contentType)); + } else { + return new BasicResponseProducer(new StringAsyncEntityProducer(Objects.toString(m.error()), ContentType.TEXT_PLAIN)); } - }) + // Map exceptions to a response message + .errorMessage(Throwable::getMessage) + // Generate a response to the request + .handle((m, context) -> { + final HttpRequest request = m.head(); + final URI requestUri; + try { + requestUri = request.getUri(); + } catch (final URISyntaxException ex) { + throw new ProtocolException(ex.getMessage(), ex); + } + final String path = requestUri.getPath(); + final File file = new File(docRoot, path); + if (!file.exists()) { + println("File " + file.getPath() + " not found"); + return Message.error(new BasicHttpResponse(HttpStatus.SC_NOT_FOUND), "File not found"); + } else if (!file.canRead() || file.isDirectory()) { + println("Cannot read file " + file.getPath()); + return Message.error(new BasicHttpResponse(HttpStatus.SC_FORBIDDEN), "File cannot be accessed"); + } else { + final HttpCoreContext coreContext = HttpCoreContext.cast(context); + final EndpointDetails endpoint = coreContext.getEndpointDetails(); + println(endpoint + " | serving file " + file.getPath()); + return Message.of(new BasicHttpResponse(HttpStatus.SC_OK), file); + } + }) + .supplier(); + + final HttpAsyncServer server = AsyncServerBootstrap.bootstrap() + .setExceptionCallback(e -> e.printStackTrace()) + .setIOReactorConfig(config) + .setRequestRouter(RequestRouter.>builder() + .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", exchangeHandlerSupplier) + .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) + .build()) .create(); Runtime.getRuntime().addShutdownHook(new Thread(() -> { @@ -177,4 +164,4 @@ static void println(final String msg) { System.out.println(HttpDateGenerator.INSTANCE.getCurrentDate() + " | " + msg); } -} +} \ No newline at end of file diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncPipelinedRequestExecutionExample.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncPipelinedRequestExecutionExample.java index 8a468f500..e67467c28 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncPipelinedRequestExecutionExample.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncPipelinedRequestExecutionExample.java @@ -42,9 +42,8 @@ import org.apache.hc.core5.http.message.RequestLine; import org.apache.hc.core5.http.message.StatusLine; import org.apache.hc.core5.http.nio.AsyncClientEndpoint; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; -import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.nio.support.AsyncClientPipeline; +import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.util.Timeout; @@ -95,43 +94,45 @@ public void onExchangeComplete(final HttpConnection connection, final boolean ke requester.start(); final HttpHost target = new HttpHost("httpbin.org"); - final String[] requestUris = new String[] {"/", "/ip", "/user-agent", "/headers"}; + final String[] requestUris = new String[]{ "/ip", "/user-agent", "/headers" }; - final Future future = requester.connect(target, Timeout.ofSeconds(5)); + final Future future = requester.connect(target, Timeout.ofSeconds(30)); final AsyncClientEndpoint clientEndpoint = future.get(); final CountDownLatch latch = new CountDownLatch(requestUris.length); - for (final String requestUri: requestUris) { + for (final String requestUri : requestUris) { clientEndpoint.execute( - AsyncRequestBuilder.get() - .setHttpHost(target) - .setPath(requestUri) - .build(), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - new FutureCallback>() { - - @Override - public void completed(final Message message) { - latch.countDown(); - final HttpResponse response = message.head(); - final String body = message.body(); - System.out.println(requestUri + "->" + response.getCode()); - System.out.println(body); - } - - @Override - public void failed(final Exception ex) { - latch.countDown(); - System.out.println(requestUri + "->" + ex); - } - - @Override - public void cancelled() { - latch.countDown(); - System.out.println(requestUri + " cancelled"); - } - - }); + AsyncClientPipeline.assemble() + .request() + .get(target, requestUri) + .response() + .asString() + .result(new FutureCallback>() { + + @Override + public void completed(final Message message) { + final HttpResponse response = message.head(); + final String body = message.body(); + System.out.println(requestUri + "->" + response.getCode()); + System.out.println(body); + latch.countDown(); + } + + @Override + public void failed(final Exception ex) { + System.out.println(requestUri + "->" + ex); + latch.countDown(); + } + + @Override + public void cancelled() { + System.out.println(requestUri + " cancelled"); + latch.countDown(); + } + + }) + .create(), + HttpCoreContext.create()); } latch.await(); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncRequestExecutionExample.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncRequestExecutionExample.java index 194959c24..3f210aa40 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncRequestExecutionExample.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncRequestExecutionExample.java @@ -40,9 +40,8 @@ import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester; import org.apache.hc.core5.http.message.RequestLine; import org.apache.hc.core5.http.message.StatusLine; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder; -import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; +import org.apache.hc.core5.http.nio.support.AsyncClientPipeline; +import org.apache.hc.core5.http.protocol.HttpCoreContext; import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.reactor.IOReactorConfig; import org.apache.hc.core5.util.Timeout; @@ -92,41 +91,45 @@ public void onExchangeComplete(final HttpConnection connection, final boolean ke requester.start(); final HttpHost target = new HttpHost("httpbin.org"); - final String[] requestUris = new String[] {"/", "/ip", "/user-agent", "/headers"}; + final String[] requestUris = new String[]{ "/ip", "/user-agent", "/headers" }; final CountDownLatch latch = new CountDownLatch(requestUris.length); - for (final String requestUri: requestUris) { + for (final String requestUri : requestUris) { requester.execute( - AsyncRequestBuilder.get() - .setHttpHost(target) - .setPath(requestUri) - .build(), - new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), - Timeout.ofSeconds(5), - new FutureCallback>() { - - @Override - public void completed(final Message message) { - final HttpResponse response = message.head(); - final String body = message.body(); - System.out.println(requestUri + "->" + response.getCode()); - System.out.println(body); - latch.countDown(); - } - - @Override - public void failed(final Exception ex) { - System.out.println(requestUri + "->" + ex); - latch.countDown(); - } - - @Override - public void cancelled() { - System.out.println(requestUri + " cancelled"); - latch.countDown(); - } - - }); + target, + AsyncClientPipeline.assemble() + .request() + .get(target, requestUri) + .response() + .asString() + .result(new FutureCallback>() { + + @Override + public void completed(final Message message) { + final HttpResponse response = message.head(); + final String body = message.body(); + System.out.println(requestUri + "->" + response.getCode()); + System.out.println(body); + latch.countDown(); + } + + @Override + public void failed(final Exception ex) { + System.out.println(requestUri + "->" + ex); + latch.countDown(); + } + + @Override + public void cancelled() { + System.out.println(requestUri + " cancelled"); + latch.countDown(); + } + + }) + .create(), + null, + Timeout.ofMinutes(1), + HttpCoreContext.create()); } latch.await(); diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncServerFilterExample.java b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncServerFilterExample.java index a89f38214..61e7985d6 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncServerFilterExample.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/examples/AsyncServerFilterExample.java @@ -31,7 +31,8 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; @@ -41,17 +42,15 @@ import org.apache.hc.core5.http.impl.bootstrap.AsyncServerBootstrap; import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; import org.apache.hc.core5.http.impl.bootstrap.StandardFilter; +import org.apache.hc.core5.http.impl.routing.RequestRouter; import org.apache.hc.core5.http.message.BasicHttpResponse; import org.apache.hc.core5.http.nio.AsyncEntityProducer; import org.apache.hc.core5.http.nio.AsyncFilterChain; import org.apache.hc.core5.http.nio.AsyncPushProducer; -import org.apache.hc.core5.http.nio.AsyncRequestConsumer; -import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; +import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; -import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; import org.apache.hc.core5.http.nio.support.AbstractAsyncServerAuthFilter; -import org.apache.hc.core5.http.nio.support.AsyncResponseBuilder; -import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; +import org.apache.hc.core5.http.nio.support.AsyncServerPipeline; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.net.URIAuthority; @@ -144,29 +143,22 @@ public void pushPromise( // Application request handler - .register("*", new AsyncServerRequestHandler>() { - - @Override - public AsyncRequestConsumer> prepare( - final HttpRequest request, - final EntityDetails entityDetails, - final HttpContext context) throws HttpException { - return new BasicRequestConsumer<>(entityDetails != null ? new StringAsyncEntityConsumer() : null); - } - - @Override - public void handle( - final Message requestMessage, - final ResponseTrigger responseTrigger, - final HttpContext context) throws HttpException, IOException { - // do something useful - responseTrigger.submitResponse( - AsyncResponseBuilder.create(HttpStatus.SC_OK) - .setEntity("Hello") - .build(), - context); - } - }) + .setRequestRouter(RequestRouter.>builder() + .addRoute(RequestRouter.LOCAL_AUTHORITY, + "*", + AsyncServerPipeline.assemble() + // Represent request as string + .request() + .asString() + // Represent response as string + .response() + .asString(ContentType.TEXT_PLAIN) + // Generate a response to the request + .handle((r, c) -> + Message.of(new BasicHttpResponse(HttpStatus.SC_OK), "Hello")) + .supplier()) + .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER) + .build()) .create(); Runtime.getRuntime().addShutdownHook(new Thread(() -> {