package com.linkedin.databus.core; /* * * Copyright 2013 LinkedIn Corp. All rights reserved * * Licensed 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. * */ import static org.jboss.netty.channel.Channels.pipeline; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK; import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertTrue; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.log4j.BasicConfigurator; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.PatternLayout; import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.bootstrap.ServerBootstrap; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelPipelineFactory; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.channel.local.DefaultLocalClientChannelFactory; import org.jboss.netty.channel.local.DefaultLocalServerChannelFactory; import org.jboss.netty.channel.local.LocalAddress; import org.jboss.netty.handler.codec.http.DefaultHttpRequest; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpClientCodec; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpRequestDecoder; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseEncoder; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.jboss.netty.handler.logging.LoggingHandler; import org.jboss.netty.logging.InternalLogLevel; import org.jboss.netty.logging.InternalLoggerFactory; import org.jboss.netty.logging.Log4JLoggerFactory; import org.testng.annotations.AfterMethod; import org.testng.annotations.Test; import org.testng.annotations.BeforeMethod; import com.linkedin.databus.core.test.netty.FooterAwareHttpChunkAggregator; import com.linkedin.databus.core.test.netty.SimpleHttpResponseHandler; import com.linkedin.databus2.core.container.netty.ChunkedBodyWritableByteChannel; public class TestChunkedBodyWritableByteChannel { public static final String MODULE = TestChunkedBodyWritableByteChannel.class.getName(); public static final Logger LOG = Logger.getLogger(MODULE); private ServerBootstrap _serverBootstrap; private ClientBootstrap _clientBootstrap; private SimpleHttpResponseHandler _responseHandler; private LocalAddress _serverAddress; private Channel _serverChannel; private ServerThread _serverThread; private ClientThread _clientThread; static { BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d{ISO8601} %c{2} [%p] %m%n"), "System.err")); Logger.getRootLogger().setLevel(Level.OFF); //Logger.getRootLogger().setLevel(Level.ERROR); //Logger.getRootLogger().setLevel(Level.INFO); //Uncomment the following line to see the dumps of the data exchanges between the server and the //client //Logger.getRootLogger().setLevel(Level.DEBUG); InternalLoggerFactory.setDefaultFactory(new Log4JLoggerFactory()); } @BeforeMethod public void setUp() throws Exception { } @AfterMethod public void tearDown() throws Exception { if (null != _clientThread) { _clientThread.stopClient(); _clientThread.join(); } if (null != _serverThread) { _serverThread.stopServer(); _serverThread.join(); } else { _serverChannel.close(); } _serverBootstrap.releaseExternalResources(); _clientBootstrap.releaseExternalResources(); } @Test public void testWriteEmptyResponse() { LOG.info("Start: Testing empty response"); setupClient(); ArrayList<byte[]> chunks = new ArrayList<byte[]>(); HashMap<String, String> headers = new HashMap<String, String>(); HashMap<String, String> footers = new HashMap<String, String>(); setupServer(HttpResponseStatus.OK,chunks, headers, footers); ChannelFuture connectFuture = _clientBootstrap.connect(_serverAddress); connectFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); assertTrue("connect succeeded", connectFuture.isSuccess()); HttpRequest request = new DefaultHttpRequest(HTTP_1_1, HttpMethod.GET, "/test"); Channel requestChannel = connectFuture.getChannel(); ChannelFuture writeFuture = requestChannel.write(request); writeFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); assertTrue("connect succeeded", writeFuture.isSuccess()); HttpResponse response = _responseHandler.getResponse(); assertEquals("response code", HttpResponseStatus.OK, response.getStatus()); byte[] responseBody = _responseHandler.getReceivedBytes(); assertTrue("empty response", null == responseBody || responseBody.length == 0); LOG.info("Done: Testing empty response"); } @Test public void testWriteHeadersEmptyBody() { LOG.info("Start: Testing headers with empty body"); setupClient(); ArrayList<byte[]> chunks = new ArrayList<byte[]>(); HashMap<String, String> headers = new HashMap<String, String>(); headers.put("header1", "value1"); headers.put("header2", "value2"); HashMap<String, String> footers = new HashMap<String, String>(); setupServer(HttpResponseStatus.OK,chunks, headers, footers); ChannelFuture connectFuture = _clientBootstrap.connect(_serverAddress); connectFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); assertTrue("connect succeeded", connectFuture.isSuccess()); HttpRequest request = new DefaultHttpRequest(HTTP_1_1, HttpMethod.GET, "/test"); Channel requestChannel = connectFuture.getChannel(); ChannelFuture writeFuture = requestChannel.write(request); writeFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); assertTrue("connect succeeded", writeFuture.isSuccess()); HttpResponse response = _responseHandler.getResponse(); assertEquals("response code", HttpResponseStatus.OK, response.getStatus()); assertEquals("Checking header1 value", "value1", response.getHeader("header1")); assertEquals("Checking header2 value", "value2", response.getHeader("header2")); assertEquals("Checking header3 value", null, response.getHeader("header3")); byte[] responseBody = _responseHandler.getReceivedBytes(); assertTrue("empty response", null == responseBody || responseBody.length == 0); LOG.info("Done: Testing headers with empty body"); } @Test public void testWriteOneChunk() { LOG.info("Start: Testing headers with one chunk"); setupClient(); String chunk1 = "hello"; ArrayList<byte[]> chunks = new ArrayList<byte[]>(); chunks.add(chunk1.getBytes(Charset.defaultCharset())); HashMap<String, String> headers = new HashMap<String, String>(); headers.put("header1", "value1"); headers.put("header2", "value2"); HashMap<String, String> footers = new HashMap<String, String>(); setupServer(HttpResponseStatus.OK, chunks, headers, footers); ChannelFuture connectFuture = _clientBootstrap.connect(_serverAddress); connectFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); assertTrue("connect succeeded", connectFuture.isSuccess()); HttpRequest request = new DefaultHttpRequest(HTTP_1_1, HttpMethod.GET, "/test"); Channel requestChannel = connectFuture.getChannel(); ChannelFuture writeFuture = requestChannel.write(request); writeFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); assertTrue("connect succeeded", writeFuture.isSuccess()); HttpResponse response = _responseHandler.getResponse(); assertEquals("response code", Integer.toString(HttpResponseStatus.OK.getCode()), response.getHeader(ChunkedBodyWritableByteChannel.RESPONSE_CODE_FOOTER_NAME)); assertEquals("Checking header1 value", "value1", response.getHeader("header1")); assertEquals("Checking header2 value", "value2", response.getHeader("header2")); byte[] responseBody = _responseHandler.getReceivedBytes(); assertEquals("response length", chunk1.getBytes(Charset.defaultCharset()).length, responseBody.length); assertTrue("response content", Arrays.equals(chunk1.getBytes(Charset.defaultCharset()), responseBody)); LOG.info("Done: Testing headers with one chunk"); } @Test public void testWriteTwoChunks() { LOG.info("Start: Testing headers with one chunk"); setupClient(); String chunk1 = "hello"; String chunk2 = "bye"; ArrayList<byte[]> chunks = new ArrayList<byte[]>(); chunks.add(chunk1.getBytes(Charset.defaultCharset())); chunks.add(chunk2.getBytes(Charset.defaultCharset())); HashMap<String, String> headers = new HashMap<String, String>(); headers.put("header1", "value1"); headers.put("header2", "value2"); HashMap<String, String> footers = new HashMap<String, String>(); footers.put("footer1", "1value"); footers.put("footer2", "2value"); setupServer(HttpResponseStatus.OK, chunks, headers, footers); ChannelFuture connectFuture = _clientBootstrap.connect(_serverAddress); connectFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); assertTrue("connect succeeded", connectFuture.isSuccess()); HttpRequest request = new DefaultHttpRequest(HTTP_1_1, HttpMethod.GET, "/test"); Channel requestChannel = connectFuture.getChannel(); ChannelFuture writeFuture = requestChannel.write(request); writeFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); assertTrue("connect succeeded", writeFuture.isSuccess()); HttpResponse response = _responseHandler.getResponse(); assertEquals("response code", Integer.toString(HttpResponseStatus.OK.getCode()), response.getHeader(ChunkedBodyWritableByteChannel.RESPONSE_CODE_FOOTER_NAME)); assertEquals("Checking header1 value", "value1", response.getHeader("header1")); assertEquals("Checking header2 value", "value2", response.getHeader("header2")); assertEquals("Checking footer1 value", "1value", response.getHeader("footer1")); assertEquals("Checking footer2 value", "2value", response.getHeader("footer2")); byte[] responseBody = _responseHandler.getReceivedBytes(); assertEquals("response length", chunk1.getBytes(Charset.defaultCharset()).length + chunk2.getBytes(Charset.defaultCharset()).length, responseBody.length); byte[] fullBody = new byte[chunk1.getBytes(Charset.defaultCharset()).length + chunk2.getBytes(Charset.defaultCharset()).length]; System.arraycopy(chunk1.getBytes(Charset.defaultCharset()), 0, fullBody, 0, chunk1.getBytes(Charset.defaultCharset()).length); System.arraycopy(chunk2.getBytes(Charset.defaultCharset()), 0, fullBody, chunk1.getBytes(Charset.defaultCharset()).length, chunk2.getBytes(Charset.defaultCharset()).length); assertTrue("response content", Arrays.equals(fullBody, responseBody)); LOG.info("Done: Testing headers with one chunk"); } @Test public void testSetResponseCode() { LOG.info("Start: Testing response code with headers with empty body"); setupClient(); ArrayList<byte[]> chunks = new ArrayList<byte[]>(); HashMap<String, String> headers = new HashMap<String, String>(); headers.put("header1", "value1"); headers.put("header2", "value2"); HashMap<String, String> footers = new HashMap<String, String>(); setupServer(HttpResponseStatus.BAD_GATEWAY, chunks, headers, footers); ChannelFuture connectFuture = _clientBootstrap.connect(_serverAddress); connectFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); assertTrue("connect succeeded", connectFuture.isSuccess()); HttpRequest request = new DefaultHttpRequest(HTTP_1_1, HttpMethod.GET, "/test"); Channel requestChannel = connectFuture.getChannel(); ChannelFuture writeFuture = requestChannel.write(request); writeFuture.awaitUninterruptibly(1, TimeUnit.SECONDS); assertTrue("connect succeeded", writeFuture.isSuccess()); HttpResponse response = _responseHandler.getResponse(); assertEquals("response code", HttpResponseStatus.BAD_GATEWAY, response.getStatus()); assertEquals("Checking header1 value", "value1", response.getHeader("header1")); assertEquals("Checking header2 value", "value2", response.getHeader("header2")); assertEquals("Checking header3 value", null, response.getHeader("header3")); byte[] responseBody = _responseHandler.getReceivedBytes(); assertTrue("empty response", null == responseBody || responseBody.length == 0); LOG.info("Done: Testing response code with headers with empty body"); } private void setupServerThread(HttpResponseStatus responseCode, List<byte[]> responseChunks, Map<String, String> headers, Map<String, String> footers) { DummyHttpRequestHandler requestHandler = new DummyHttpRequestHandler(responseCode, responseChunks, headers, footers); _serverThread = new ServerThread(requestHandler); _serverThread.start(); } private void setupServer(HttpResponseStatus responseCode, List<byte[]> responseChunks, Map<String, String> headers, Map<String, String> footers) { DummyHttpRequestHandler requestHandler = new DummyHttpRequestHandler(responseCode, responseChunks, headers, footers); setupServer(requestHandler); } private void setupServer(DummyHttpRequestHandler requestHandler) { _serverBootstrap = new ServerBootstrap(new DefaultLocalServerChannelFactory()); ChannelPipeline serverPipeline = pipeline(); serverPipeline.addLast("server logger 1", new LoggingHandler("server logger 1", InternalLogLevel.DEBUG, true)); serverPipeline.addLast("decoder", new HttpRequestDecoder()); serverPipeline.addLast("encoder", new HttpResponseEncoder()); serverPipeline.addLast("server loggger 5", new LoggingHandler("server logger 5", InternalLogLevel.DEBUG, true)); serverPipeline.addLast("handler", requestHandler); _serverBootstrap.setPipeline(serverPipeline); _serverAddress = new LocalAddress(1); _serverChannel = _serverBootstrap.bind(_serverAddress); } private void setupClient() { _clientBootstrap = new ClientBootstrap(new DefaultLocalClientChannelFactory()); _clientBootstrap.setPipelineFactory(new ChannelPipelineFactory() { @Override public ChannelPipeline getPipeline() throws Exception { ChannelPipeline clientPipeline = pipeline(); clientPipeline.addLast("client logger 1", new LoggingHandler("client logger 1", InternalLogLevel.DEBUG, true)); clientPipeline.addLast("codec", new HttpClientCodec()); clientPipeline.addLast("aggregator", new FooterAwareHttpChunkAggregator(1000000)); _responseHandler = new SimpleHttpResponseHandler(); clientPipeline.addLast("handler", _responseHandler); clientPipeline.addLast("client logger 5", new LoggingHandler("client logger 5", InternalLogLevel.DEBUG, true)); return clientPipeline; } }); } private void setupClientThread() { _clientThread = new ClientThread(); _clientThread.start(); } class ServerThread extends Thread { private AtomicBoolean _stop; public ServerThread(DummyHttpRequestHandler requestHandler) { super("server thread"); setupServer(requestHandler); } public void stopServer() { _stop.set(true); _stop.notifyAll(); } @Override public void start() { super.start(); } @Override public void run() { LOG.info("Server thread started"); _stop = new AtomicBoolean(false); while (! _stop.get()) { try {_stop.wait();} catch (InterruptedException ie) {} } _serverChannel.close(); LOG.info("Server thread stopped"); } } class ClientThread extends Thread { private AtomicBoolean _stop; public ClientThread() { setupClient(); } @Override public void start() { super.start(); } public void stopClient() { _stop.set(true); _stop.notifyAll(); } @Override public void run() { LOG.info("Client thread started"); _stop = new AtomicBoolean(false); while (! _stop.get()) { try {this.wait();} catch (InterruptedException ie) {} } LOG.info("Client thread stopped"); } } } class DummyHttpRequestHandler extends SimpleChannelUpstreamHandler { private final List<byte[]> _responseChunks; private final Map<String, String> _headers; private final Map<String, String> _footers; private final HttpResponseStatus _responseCode; public DummyHttpRequestHandler(HttpResponseStatus responseCode, List<byte[]> responseChunks, Map<String, String> headers, Map<String, String> footers) { _responseChunks = responseChunks; _headers = headers; _footers = footers; _responseCode = responseCode; } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { //HttpRequest request = (HttpRequest) e.getMessage(); HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8"); ChunkedBodyWritableByteChannel writeChannel = new ChunkedBodyWritableByteChannel(e.getChannel(), response); for (String key: _headers.keySet()) { writeChannel.setMetadata(key, _headers.get(key)); } for (byte[] chunk: _responseChunks) { writeChannel.write(ByteBuffer.wrap(chunk)); } writeChannel.setResponseCode(_responseCode); for (String key: _footers.keySet()) { writeChannel.setMetadata(key, _footers.get(key)); } writeChannel.close(); } }