// // Copyright 2010 Cinch Logic Pty Ltd. // // http://www.chililog.com // // 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. // package org.chililog.server.pubsub.jsonhttp; import static org.jboss.netty.handler.codec.http.HttpHeaders.*; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*; import static org.jboss.netty.handler.codec.http.HttpMethod.*; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*; import static org.jboss.netty.handler.codec.http.HttpVersion.*; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.concurrent.Executor; import org.apache.commons.lang.StringUtils; import org.chililog.server.common.Log4JLogger; import org.chililog.server.pubsub.websocket.CloseWebSocketFrame; import org.chililog.server.pubsub.websocket.PingWebSocketFrame; import org.chililog.server.pubsub.websocket.PongWebSocketFrame; import org.chililog.server.pubsub.websocket.TextWebSocketFrame; import org.chililog.server.pubsub.websocket.WebSocketFrame; import org.chililog.server.pubsub.websocket.WebSocketServerHandshaker; import org.chililog.server.pubsub.websocket.WebSocketServerHandshakerFactory; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelStateEvent; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.util.CharsetUtil; /** * Handler for JSON log entries send over HTTP request and web socket. * * @author vibul */ public class JsonHttpRequestHandler extends SimpleChannelUpstreamHandler { private Executor _executor = null; private static Log4JLogger _logger = Log4JLogger.getLogger(JsonHttpRequestHandler.class); private static final String PUBLISH_PATH = "/publish"; private static final String WEBSOCKET_PATH = "/websocket"; private static final Charset UTF_8_CHARSET = Charset.forName("UTF-8"); private SubscriptionWorker _subscriptionWorker = null; private WebSocketServerHandshaker _handshaker = null; /** * Constructor * * @param executor * ThreadPool to use for processing requests */ public JsonHttpRequestHandler(Executor executor) { _executor = executor; } /** * Handles incoming HTTP data * * @param ctx * Channel Handler Context * @param e * Message event */ @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { Object msg = e.getMessage(); if (msg instanceof HttpRequest) { handleHttpRequest(ctx, e, (HttpRequest) msg); } else if (msg instanceof WebSocketFrame) { handleWebSocketFrame(ctx, (WebSocketFrame) msg); } else { throw new UnsupportedOperationException("Message of type '" + msg.getClass().getName() + "' is not supported."); } } /** * <p> * Handles HTTP requests. * </p> * <p> * HTTP requests can only be for web socket handshake or for JSON log publishing * </p> * * @param ctx * Channel Handler Context * @param e * Message event * @param req * HTTP request * @throws Exception */ private void handleHttpRequest(final ChannelHandlerContext ctx, final MessageEvent e, final HttpRequest req) throws Exception { // TODO should invoke workers in a different thread pool to improve performance if (req.getMethod() == GET) { // Web socket handshake if (req.getUri().equals(WEBSOCKET_PATH)) { String wsURL = "ws://" + req.getHeader(HttpHeaders.Names.HOST) + WEBSOCKET_PATH; WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(wsURL, null, false); _handshaker = wsFactory.newHandshaker(ctx, req); if (_handshaker == null) { wsFactory.sendUnsupportedWebSocketVersionResponse(ctx); } else { _handshaker.executeOpeningHandshake(ctx, req); } return; } } else if (req.getMethod() == POST && req.getUri().equals(PUBLISH_PATH)) { // Get request content final ChannelBuffer content = req.getContent(); if (!content.readable()) { throw new IllegalStateException("HTTP request content is NOT readable"); } _executor.execute(new Runnable() { @Override public void run() { try { byte[] requestContent = content.array(); String requestJson = bytesToString(requestContent); PublicationWorker worker = new PublicationWorker(JsonHttpService.getInstance() .getMqProducerSessionPool()); StringBuilder responseJson = new StringBuilder(); _logger.debug("Channel %s Publication Worker Request:\n%s", ctx.getChannel().getId(), requestJson); boolean success = worker.process(requestJson, responseJson); _logger.debug("Channel %s Publication Worker Response:\n%s", ctx.getChannel().getId(), responseJson); HttpResponse res = success ? new DefaultHttpResponse(HTTP_1_1, OK) : new DefaultHttpResponse( HTTP_1_1, BAD_REQUEST); res.setHeader(CONTENT_TYPE, "application/json; charset=UTF-8"); // Cross Domain: // http://www.nczonline.net/blog/2010/05/25/cross-domain-ajax-with-cross-origin-resource-sharing/ res.setHeader("Access-Control-Allow-Origin", "*"); res.setContent(ChannelBuffers.copiedBuffer(responseJson.toString(), UTF_8_CHARSET)); sendHttpResponse(ctx, req, res); return; } catch (Exception exception) { _logger.debug(exception, "Error handling PubSub JSON HTTP Request"); e.getChannel().close(); } } }); return; } // Only GET and POST are permitted sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN)); return; } /** * Converts request bytes into a string using the default UTF-8 character set. * * @param bytes * Bytes to convert * @return String form the bytes. If bytes is null, null is returned. * @throws UnsupportedEncodingException */ private String bytesToString(byte[] bytes) throws UnsupportedEncodingException { if (bytes == null) { return null; } return new String(bytes, UTF_8_CHARSET); } /** * <p> * Figure out if this is a publish or subscribe request and do it. * </p> * * @param ctx * @param frame */ private void handleWebSocketFrame(final ChannelHandlerContext ctx, final WebSocketFrame frame) { _logger.debug("Channel %s got %s frame.", ctx.getChannel().getId(), frame.getClass().getName()); // TODO should invoke workers in a different thread pool to improve performance // Check for closing frame if (frame instanceof CloseWebSocketFrame) { _handshaker.executeClosingHandshake(ctx, (CloseWebSocketFrame) frame); return; } else if (frame instanceof PingWebSocketFrame) { _logger.debug("PingPong"); ctx.getChannel().write(new PongWebSocketFrame(frame.getBinaryData())); return; } else if (!(frame instanceof TextWebSocketFrame)) { throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass() .getName())); } TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; final String requestJson = textFrame.getText(); if (StringUtils.isBlank(requestJson)) { return; } _executor.execute(new Runnable() { @Override public void run() { try { _logger.debug("Channel %s Request JSON: %s", ctx.getChannel().getId(), requestJson); String responseJson = null; // Process according to request type // We do a quick peek in the json in order to dispatch to the required worker String first50Characters = requestJson.length() > 50 ? requestJson.substring(0, 50) : requestJson; if (first50Characters.indexOf("\"PublicationRequest\"") > 0) { PublicationWorker worker = new PublicationWorker(JsonHttpService.getInstance() .getMqProducerSessionPool()); StringBuilder sb = new StringBuilder(); worker.process(requestJson, sb); responseJson = sb.toString(); } else if (first50Characters.indexOf("\"SubscriptionRequest\"") > 0) { // If existing subscription exists, stop it first if (_subscriptionWorker != null) { _subscriptionWorker.stop(); } _subscriptionWorker = new SubscriptionWorker(ctx.getChannel()); StringBuilder sb = new StringBuilder(); _subscriptionWorker.process(requestJson, sb); responseJson = sb.toString(); } else { throw new UnsupportedOperationException("Unsupported request: " + requestJson); } _logger.debug("Channel %s Response JSON: %s", ctx.getChannel().getId(), responseJson); ctx.getChannel().write(new TextWebSocketFrame(responseJson)); return; } catch (Exception exception) { _logger.debug(exception, "Error handling PubSub JSON HTTP Request"); ctx.getChannel().close(); } } }); } /** * Returns a HTTP response * * @param ctx * Content * @param req * HTTP request * @param res * HTTP response */ private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) { // Decide whether to close the connection or not. boolean isKeepAlive = isKeepAlive(req); // Set content if one is not set if (res.getContent() == null || res.getContent().readableBytes() == 0) { res.setContent(ChannelBuffers.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8)); setContentLength(res, res.getContent().readableBytes()); } // Add 'Content-Length' header only for a keep-alive connection. if (isKeepAlive) { res.setHeader(CONTENT_LENGTH, res.getContent().readableBytes()); } // Send the response ChannelFuture f = ctx.getChannel().write(res); // Close the connection if necessary. if (!isKeepAlive || res.getStatus().getCode() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } /** * Handle exception by printing it and closing socket */ @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { try { _logger.debug(e.getCause(), "Error handling PubSub JSON HTTP Request"); e.getChannel().close(); } catch (Exception ex) { _logger.debug(ex, "Error closing channel in exception"); } } /** * Add channel to channel group to disconnect when shutting down. Channel group automatically removes closed * channels. */ @Override public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) { JsonHttpService.getInstance().getAllChannels().add(e.getChannel()); } /** * If disconnected, stop subscription if one is present */ @Override public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { _logger.debug("Channel %s disconnected", ctx.getChannel().getId()); if (_subscriptionWorker != null) { _subscriptionWorker.stop(); } super.channelDisconnected(ctx, e); } }