content: package com.alecgorge.minecraft.jsonapi.packets.netty;
  
  import io.netty.channel.ChannelFuture;
  import io.netty.channel.ChannelFutureListener;
  import io.netty.channel.ChannelHandlerContext;
  import io.netty.channel.SimpleChannelInboundHandler;
  import io.netty.handler.codec.http.DefaultFullHttpRequest;
  import io.netty.handler.codec.http.DefaultFullHttpResponse;
  import io.netty.handler.codec.http.FullHttpRequest;
  import io.netty.handler.codec.http.FullHttpResponse;
  import io.netty.handler.codec.http.HttpHeaders;
  import io.netty.handler.codec.http.HttpMethod;
  import io.netty.handler.codec.http.HttpResponseStatus;
  import io.netty.handler.codec.http.HttpVersion;
  import io.netty.handler.codec.http.QueryStringDecoder;
  import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
  import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
  import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
  import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
  import io.netty.handler.codec.http.websocketx.WebSocketFrame;
  import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
  import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
  import io.netty.util.CharsetUtil;
  
  import java.io.IOException;
  
  import com.alecgorge.minecraft.jsonapi.JSONAPI;
  import com.alecgorge.minecraft.jsonapi.streams.StreamingResponse;
  
  class JSONAPIHandler extends SimpleChannelInboundHandler<Object> {
  	private static final String			WEBSOCKET_PATH	= "/api/2/websocket";
  
  	private WebSocketServerHandshaker	handshaker;
  	private boolean						continueSending	= true;
  
  	private JSONAPI						api;
  
  	public JSONAPIHandler(JSONAPI api) {
  		this.api = api;
  	}
  
  	@Override
  	protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
  		if (msg instanceof FullHttpRequest) {
  			handleHttpRequest(ctx, (FullHttpRequest) msg);
  		}
  		else if (msg instanceof WebSocketFrame) {
  			handleWebSocketFrame(ctx, (WebSocketFrame) msg);
  		}
  	}
  
  	@Override
  	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
  		ctx.flush();
  	}
  
  	private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
  		// Handle a bad request.
  		if (!req.getDecoderResult().isSuccess()) {
  			sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
  			return;
  		}
  
  		QueryStringDecoder uri = new QueryStringDecoder(req.getUri());
  		if (uri.path().equals(WEBSOCKET_PATH)) {
  			// Handshake
  			WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(req), null, false);
  			handshaker = wsFactory.newHandshaker(req);
  			if (handshaker == null) {
  				WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
  			}
  			else {
  				handshaker.handshake(ctx.channel(), req);
  			}
  		}
  		else {
  			api.getRouter().serveRequest(ctx, req);
  		}
  	}
  
  	private void handleWebSocketFrame(final ChannelHandlerContext ctx, WebSocketFrame frame) {
  		// Check for closing frame
  		if (frame instanceof CloseWebSocketFrame) {
  			continueSending = false;
  			handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
  			return;
  		}
  		if (frame instanceof PingWebSocketFrame) {
  			ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
  			return;
  		}
  		if (!(frame instanceof TextWebSocketFrame)) {
  			throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass().getName()));
  		}
  
  		TextWebSocketFrame txt = ((TextWebSocketFrame) frame);
  
  		FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, txt.text());
  		if (APIv2Handler.canServe(new QueryStringDecoder(req.getUri()))) {
  			APIv2Handler h = new APIv2Handler(req);
  
  			if (h.isStream()) {
  				final StreamingResponse s = h.subscribe();
  				(new Thread(new Runnable() {
  					@Override
  					public void run() {
  						String line = "";
  
  						JSONAPI.dbug("starting streaming response");
  						while (continueSending && (line = s.nextLine()) != null) {
  							ctx.channel().write(new TextWebSocketFrame(line.trim()));
  						}
  
  						try {
  							JSONAPI.dbug("closing streaming response continueSending: " + continueSending);
  							s.close();
  						}
  						catch (IOException e) {
  							e.printStackTrace();
  						}
  					}
  				})).start();
  			}
  			else {
  				ctx.channel().write(new TextWebSocketFrame(h.serve().content()));
  			}
  		}
  		else {
  			FullHttpResponse resp = api.getRouter().getResponse(ctx, req);
  			ctx.channel().write(new TextWebSocketFrame(resp.content().toString(CharsetUtil.UTF_8)));
  		}
  	}
  
  	private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
  		// Send the response and close the connection if necessary.
  		ChannelFuture f = ctx.channel().writeAndFlush(res);
  		if (!HttpHeaders.isKeepAlive(req) || res.getStatus().code() != 200) {
  			f.addListener(ChannelFutureListener.CLOSE);
  		}
  	}
  
  	@Override
  	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  		cause.printStackTrace();
  		JSONAPI.dbug("exception caught");
  		continueSending = false;
  		ctx.close();
  	}
  
  	@Override
  	public void channelInactive(ChannelHandlerContext ctx) throws Exception {
  		super.channelInactive(ctx);
  		JSONAPI.dbug("channel inactive");
  		continueSending = false;
  	}
  
  	private static String getWebSocketLocation(FullHttpRequest req) {
  		return "ws://" + req.headers().get(HttpHeaders.Names.HOST) + WEBSOCKET_PATH;
  	}
  }
  