package com.typesafe.netty.http; import akka.NotUsed; import akka.actor.ActorSystem; import akka.japi.function.Function; import akka.stream.Materializer; import akka.stream.javadsl.*; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.http.*; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.tck.IdentityProcessorVerification; import org.reactivestreams.tck.TestEnvironment; import org.testng.annotations.*; import scala.concurrent.Await; import scala.concurrent.duration.Duration; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * This identity processor verification verifies a client making requests to a server, that echos the * body back. * * The server uses the {@link HttpStreamsServerHandler}, and then exposes the messages sent/received by * that using reactive streams. So it effectively uses streams of streams. It then uses Akka streams * to actually handle the requests, echoing the bodies back in the responses as is. * * The client uses the {@link HttpStreamsClientHandler}, and then exposes the messages sent/received by * that using reactive streams, so it too is effectively a stream of streams. Here Akka streams is used * to split the String bodies into many chunks, for more interesting verification of the bodies, and then * combines all the chunks together back into a String at the end. */ public class FullStackHttpIdentityProcessorVerificationTest extends IdentityProcessorVerification<String> { private NioEventLoopGroup eventLoop; private Channel serverBindChannel; private ActorSystem actorSystem; private Materializer materializer; private HttpHelper helper; private ExecutorService executorService; public FullStackHttpIdentityProcessorVerificationTest() { super(new TestEnvironment(1000)); } @BeforeClass public void start() throws Exception { executorService = Executors.newCachedThreadPool(); actorSystem = ActorSystem.create(); materializer = Materializer.matFromSystem(actorSystem); helper = new HttpHelper(materializer); eventLoop = new NioEventLoopGroup(); ProcessorHttpServer server = new ProcessorHttpServer(eventLoop); // A flow that echos HttpRequest bodies in HttpResponse bodies final Flow<HttpRequest, HttpResponse, NotUsed> flow = Flow.<HttpRequest>create().map( new Function<HttpRequest, HttpResponse>() { public HttpResponse apply(HttpRequest request) throws Exception { return helper.echo(request); } } ); serverBindChannel = server.bind(new InetSocketAddress("127.0.0.1", 0), new Callable<Processor<HttpRequest, HttpResponse>>() { @Override public Processor<HttpRequest, HttpResponse> call() throws Exception { return AkkaStreamsUtil.flowToProcessor(flow, materializer); } }).await().channel(); } @AfterClass public void stop() throws Exception { serverBindChannel.close().await(); executorService.shutdown(); Await.ready(actorSystem.terminate(), Duration.create(10000, TimeUnit.MILLISECONDS)); eventLoop.shutdownGracefully(100, 10000, TimeUnit.MILLISECONDS).await(); } @Override public Processor<String, String> createIdentityProcessor(int bufferSize) { ProcessorHttpClient client = new ProcessorHttpClient(eventLoop); Processor<HttpRequest, HttpResponse> connection = getProcessor(client); Flow<String, String, ?> flow = Flow.<String>create() // Convert the Strings to HttpRequests .map(new Function<String, HttpRequest>() { @Override public HttpRequest apply(String body) throws Exception { List<String> content = new ArrayList<>(); String[] chunks = body.split(":"); for (String chunk: chunks) { // Make sure we put the ":" back into the body String c; if (content.isEmpty()) { c = chunk; } else { c = ":" + chunk; } content.add(c); } return helper.createChunkedRequest("POST", "/" + chunks[0], content); } }) // Send the flow via the HTTP client connection .via(AkkaStreamsUtil.processorToFlow(connection)) // Convert the responses to Strings .mapAsync(4, new Function<HttpResponse, CompletionStage<String>>() { @Override public CompletionStage<String> apply(HttpResponse response) throws Exception { return helper.extractBodyAsync(response); } }); return AkkaStreamsUtil.flowToProcessor(flow, materializer); } private Processor<HttpRequest, HttpResponse> getProcessor(ProcessorHttpClient client) { try { return client.connect(serverBindChannel.localAddress()); } catch (Exception ex) { throw new RuntimeException(ex); } } @Override public Publisher<String> createFailedPublisher() { return Source.<String>failed(new RuntimeException("failed")) .toMat(Sink.<String>asPublisher(AsPublisher.WITH_FANOUT), Keep.<NotUsed, Publisher<String>>right()).run(materializer); } @Override public ExecutorService publisherExecutorService() { return executorService; } /** * We want to send a list of chunks, but the problem is, Netty may (and does) dechunk things in certain * circumstances. So, we create Strings, and we split it into chunks it in the flow, then combine all * chunks when it gets back, then split again, that way if netty combines the chunks, it doesn't matter. */ @Override public String createElement(int element) { StringBuilder sb = new StringBuilder(); // Make the first element the number, then we set the URL to that number when we make the request, // making debugging easier. sb.append(element); for (int i = 0; i < 20; i++) { sb.append(":this is a very cool element, it is element number ").append(i); } return sb.toString(); } @Override public long maxSupportedSubscribers() { return 1; } }