// This software is released into the Public Domain. See copying.txt for details.
package org.openstreetmap.osmosis.replicationhttp.v0_6.impl;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.logging.Level;
import java.util.logging.Logger;
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.Channels;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
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.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.util.CharsetUtil;
/**
* Netty handler for sending replication sequence numbers to clients.
*
* @author Brett Henderson
*/
public abstract class SequenceServerHandler extends SimpleChannelHandler {
private static final Logger LOG = Logger.getLogger(SequenceServerHandler.class.getName());
private SequenceServerControl control;
private long currentSequenceNumber;
/**
* Creates a new instance.
*
* @param control
* Provides the Netty handlers with access to the controller.
*/
public SequenceServerHandler(SequenceServerControl control) {
this.control = control;
}
/**
* Gets the central control object.
*
* @return The controller.
*/
protected SequenceServerControl getControl() {
return control;
}
@Override
public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) {
control.registerChannel(e.getChannel());
}
/**
* Writes a HTTP 404 response to the client.
*
* @param ctx
* The Netty context.
* @param requestedUri
* The URI requested by the client.
*/
private void writeResourceNotFound(final ChannelHandlerContext ctx, String requestedUri) {
// Write the HTTP header to the client.
DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.NOT_FOUND);
response.headers().add("Content-Type", "text/plain");
// Send the 404 message to the client.
ChannelBuffer buffer = ChannelBuffers.copiedBuffer("The requested resource does not exist: " + requestedUri,
CharsetUtil.UTF_8);
response.setContent(buffer);
// Write the header. Use a new future because the future we've been
// passed is for upstream.
ChannelFuture headerFuture = Channels.future(ctx.getChannel());
Channels.write(ctx, headerFuture, response);
// Wait for the previous operation to finish and then close the channel.
headerFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
ctx.getChannel().close();
}
});
}
/**
* Writes a HTTP 410 response to the client.
*
* @param ctx
* The Netty context.
* @param requestedUri
* The URI requested by the client.
*/
private void writeResourceGone(final ChannelHandlerContext ctx, String requestedUri) {
// Write the HTTP header to the client.
DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.GONE);
response.headers().add("Content-Type", "text/plain");
// Send the 410 message to the client.
ChannelBuffer buffer = ChannelBuffers.copiedBuffer("The requested resource is no longer available: "
+ requestedUri, CharsetUtil.UTF_8);
response.setContent(buffer);
// Write the header. Use a new future because the future we've been
// passed is for upstream.
ChannelFuture headerFuture = Channels.future(ctx.getChannel());
Channels.write(ctx, headerFuture, response);
// Wait for the previous operation to finish and then close the channel.
headerFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
ctx.getChannel().close();
}
});
}
/**
* Writes a HTTP 400 response to the client.
*
* @param ctx
* The Netty context.
* @param requestedUri
* The URI requested by the client.
* @param errorMessage
* Further information about why the request is bad.
*/
private void writeBadRequest(final ChannelHandlerContext ctx, String requestedUri,
String errorMessage) {
final String newLine = "\r\n";
// Write the HTTP header to the client.
DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.NOT_FOUND);
response.headers().add("Content-Type", "text/plain");
// Send the 400 message to the client.
StringBuilder messageBuilder = new StringBuilder();
messageBuilder.append("Bad Request").append(newLine);
messageBuilder.append("Message: ").append(errorMessage).append(newLine);
messageBuilder.append("Requested URI: ").append(requestedUri).append(newLine);
ChannelBuffer buffer = ChannelBuffers.copiedBuffer(messageBuilder.toString(), CharsetUtil.UTF_8);
response.setContent(buffer);
// Write the header. Use a new future because the future we've been
// passed is for upstream.
ChannelFuture headerFuture = Channels.future(ctx.getChannel());
Channels.write(ctx, headerFuture, response);
// Wait for the previous operation to finish and then close the channel.
headerFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
ctx.getChannel().close();
}
});
}
/**
* Writes server statistics to the client.
*
* @param ctx
* The Netty context.
*/
private void writeStatistics(final ChannelHandlerContext ctx) {
final String newLine = "\r\n";
ServerStatistics statistics = control.getStatistics();
// Write the HTTP header to the client.
DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.OK);
response.headers().add("Content-Type", "text/plain");
// Send the statistics message to the client.
StringBuilder messageBuilder = new StringBuilder();
messageBuilder.append("Server Statistics").append(newLine);
messageBuilder.append("Total Requests: ").append(statistics.getTotalRequests()).append(newLine);
messageBuilder.append("Active Connections: ").append(statistics.getActiveConnections()).append(newLine);
ChannelBuffer buffer = ChannelBuffers.copiedBuffer(messageBuilder.toString(), CharsetUtil.UTF_8);
response.setContent(buffer);
// Write the header. Use a new future because the future we've been
// passed is for upstream.
ChannelFuture headerFuture = Channels.future(ctx.getChannel());
Channels.write(ctx, headerFuture, response);
// Wait for the previous operation to finish and then close the channel.
headerFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
ctx.getChannel().close();
}
});
}
/**
* Writes sequence data to the client. If follow is set, it allows
* continuous updates to be streamed to the client.
*
* @param ctx
* The Netty context.
* @param contentType
* The content type to set on the HTTP response.
* @param requestedSequenceNumber
* The requested sequence number. Sending will start from this
* number.
* @param follow
* If true, continuous updates will be sent to the client.
*/
protected void initiateSequenceWriting(final ChannelHandlerContext ctx,
String contentType, final long requestedSequenceNumber, final boolean follow) {
// Create the HTTP header to send to the client.
DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.headers().add("Content-Type", contentType);
response.setChunked(true);
response.headers().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
// Write the header. We must use a new future because the future we've
// been passed is for upstream.
ChannelFuture headerFuture = Channels.future(ctx.getChannel());
Channels.write(ctx, headerFuture, response);
// Wait for the previous operation to finish and then start sending
// sequence numbers to this channel.
headerFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
control.determineNextChannelAction(ctx.getChannel(), requestedSequenceNumber, follow);
}
}
});
}
/**
* Parses the request and initialises the response processing, typically by
* calling the writeSequence method.
*
* @param ctx
* The Netty context.
* @param request
* The client request.
*/
protected abstract void handleRequest(ChannelHandlerContext ctx, HttpRequest request);
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
// We have received a message from the client which is a HTTP request.
HttpRequest request = (HttpRequest) e.getMessage();
InetSocketAddress remoteAddress = (InetSocketAddress) ctx.getChannel().getRemoteAddress();
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Received new request from " + remoteAddress.getAddress().getHostAddress() + ":"
+ remoteAddress.getPort());
}
// Process the HTTP request.
try {
// Check if this is a request to a generic URL. If it isn't
// something we support then delegate to the specific handler.
if (request.getUri().equals("/statistics")) {
writeStatistics(ctx);
} else {
handleRequest(ctx, request);
}
} catch (ResourceNotFoundException ex) {
writeResourceNotFound(ctx, request.getUri());
} catch (ResourceGoneException ex) {
writeResourceGone(ctx, request.getUri());
} catch (BadRequestException ex) {
writeBadRequest(ctx, request.getUri(), ex.getMessage());
}
}
/**
* Convert the sequence number to sequence data and write to the channel.
*
* @param ctx
* The channel handler context.
* @param future
* The future for current processing.
* @param sequenceNumber
* The sequence number to be written.
*/
protected abstract void writeSequence(ChannelHandlerContext ctx, ChannelFuture future, long sequenceNumber);
@Override
public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
// The message event is a Long containing the sequence number.
currentSequenceNumber = (Long) e.getMessage();
// Call the concrete implementation to convert the sequence to writable
// data.
writeSequence(ctx, e.getFuture(), currentSequenceNumber);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
// Get the cause of the exception.
Throwable t = e.getCause();
// A ClosedChannelException occurs if the client disconnects and is not
// an error scenario.
if (!(t instanceof ClosedChannelException)) {
LOG.log(Level.SEVERE, "Error during processing for channel " + ctx.getChannel() + ".", t);
}
// We must stop sending to this client if any errors occur during
// processing.
e.getChannel().close();
}
/**
* Used during request parsing to notify that the requested URI could not be
* found.
*/
protected static class ResourceNotFoundException extends RuntimeException {
private static final long serialVersionUID = -1L;
}
/**
* Used during request parsing to notify that the request is invalid in some
* way.
*/
protected static class BadRequestException extends RuntimeException {
private static final long serialVersionUID = -1L;
/**
* Creates a new instance.
*
* @param message
* The error message.
*/
public BadRequestException(String message) {
super(message);
}
}
/**
* Used during request parsing to notify that the requested URI is no longer
* available.
*/
protected static class ResourceGoneException extends RuntimeException {
private static final long serialVersionUID = -1L;
}
}