package org.jboss.netty.handler.codec.http;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.handwerkszeug.riak.Markers;
import org.handwerkszeug.riak.util.StringUtil;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author taichi
*/
public class MultipartResponseDecoder extends SimpleChannelUpstreamHandler {
static final Logger LOG = LoggerFactory
.getLogger(MultipartResponseDecoder.class);
String dashBoundary;
String closeBoundary;
State state;
ContentRange contentRange;
PartMessage readContinue;
public MultipartResponseDecoder() {
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
throws Exception {
Object o = e.getMessage();
if (o instanceof HttpResponse) {
this.dashBoundary = null;
this.closeBoundary = null;
HttpResponse response = (HttpResponse) o;
if (response.isChunked()) {
setUpBoundary(response);
} else if (setUpBoundary(response)) {
ChannelBuffer buffer = response.getContent().copy();
response.setContent(ChannelBuffers.EMPTY_BUFFER);
Channels.fireMessageReceived(ctx, response,
e.getRemoteAddress());
splitMultipart(ctx, e, buffer);
return;
}
} else if (o instanceof HttpChunk) {
HttpChunk chunk = (HttpChunk) o;
if (this.contentRange != null
&& State.READ_CHUNKD_CONTENT.equals(this.state)) {
if (this.contentRange.pass(chunk.getContent())) {
ctx.sendUpstream(e);
return;
} else {
this.contentRange = null;
this.state = State.SKIP_CONTROL_CHARS;
}
}
if (this.dashBoundary != null) {
if (chunk.isLast()) {
clearBoudndary();
} else {
ChannelBuffer buffer = chunk.getContent();
if (2 < buffer.readableBytes()) {
splitMultipart(ctx, e, buffer);
}
}
return;
}
}
ctx.sendUpstream(e);
}
public boolean setUpBoundary(HttpMessage response) {
String b = getBoundary(response);
if (b != null) {
this.dashBoundary = "--" + b;
this.closeBoundary = this.dashBoundary + "--";
this.state = State.SKIP_CONTROL_CHARS;
return true;
}
return false;
}
public void clearBoudndary() {
this.dashBoundary = null;
this.closeBoundary = null;
}
static final Pattern MultiPart = Pattern
.compile(
"multipart/[a-z]+;\\s*boundary=(([\\w'\\(\\),-./:=\\?]{1,70})|\"([\\w'\\(\\),-./:=\\?]{1,70})\")",
Pattern.CASE_INSENSITIVE);
protected String getBoundary(HttpMessage response) {
String type = response.getHeader(HttpHeaders.Names.CONTENT_TYPE);
if (type != null && type.isEmpty() == false) {
Matcher m = MultiPart.matcher(type);
String b = null;
if (m.find()) {
b = m.group(2);
if (b == null) {
b = m.group(3);
}
}
return b;
}
return null;
}
protected void splitMultipart(ChannelHandlerContext ctx, MessageEvent e,
ChannelBuffer buffer) {
while (buffer.readable()) {
PartMessage msg = parse(buffer);
if (State.READ_CONTENT.equals(this.state)) {
break;
}
Channels.fireMessageReceived(ctx, msg, e.getRemoteAddress());
if (State.READ_CHUNKD_CONTENT.equals(this.state)) {
break;
}
}
}
enum State {
SKIP_CONTROL_CHARS, READ_BOUNDARY, READ_HEADERS, READ_CONTENT, READ_CHUNKD_CONTENT, EPILOGUE;
}
public PartMessage parse(ChannelBuffer buffer) {
DefaultPartMessage multipart = new DefaultPartMessage();
for (;;) {
switch (this.state) {
case SKIP_CONTROL_CHARS: {
skipControlCharacters(buffer);
this.state = State.READ_BOUNDARY;
break;
}
case READ_BOUNDARY: {
this.state = readBoundary(buffer);
break;
}
case READ_HEADERS: {
readHeaders(buffer, multipart);
break;
}
case READ_CHUNKD_CONTENT: {
return multipart;
}
case READ_CONTENT: {
int length = seekNextBoundary(buffer);
ChannelBuffer newone = buffer.readBytes(length);
if (this.readContinue != null) {
ChannelBuffer cb = this.readContinue.getContent();
newone = ChannelBuffers.wrappedBuffer(cb, newone);
}
multipart.setContent(newone);
if (State.READ_CONTENT.equals(this.state)) {
this.readContinue = multipart;
} else {
this.readContinue = null;
}
return multipart;
}
case EPILOGUE: {
multipart.setLast(true);
this.state = State.SKIP_CONTROL_CHARS;
return multipart;
}
default: {
throw new IllegalStateException("Unknown state " + this.state);
}
}
}
}
private void skipControlCharacters(ChannelBuffer buffer) {
while (buffer.readable()) {
char c = (char) buffer.readUnsignedByte();
if (Character.isISOControl(c) == false
&& Character.isWhitespace(c) == false) {
buffer.readerIndex(buffer.readerIndex() - 1);
break;
}
}
}
private State readBoundary(ChannelBuffer buffer) {
String line = readLine(buffer);
if (this.dashBoundary.equals(line)) {
return State.READ_HEADERS;
} else if (line.equals(this.closeBoundary)) {
return State.EPILOGUE;
}
LOG.debug(Markers.DETAIL, line);
LOG.debug(Markers.DETAIL, this.dashBoundary);
throw new UnknownBoundaryException(line);
}
private void readHeaders(ChannelBuffer buffer, DefaultPartMessage message) {
String line = readLine(buffer);
String name = null;
String value = null;
do {
char firstChar = line.charAt(0);
if (name != null && (firstChar == ' ' || firstChar == '\t')) {
value = value + ' ' + line.trim();
} else {
if (name != null) {
message.addHeader(name, value);
}
String[] header = splitHeader(line);
name = header[0];
value = header[1];
}
line = readLine(buffer);
} while (line.isEmpty() == false && buffer.readable());
if (name != null) {
message.addHeader(name, value);
}
String rangeHeader = message.getHeader(HttpHeaders.Names.CONTENT_RANGE);
if (StringUtil.isEmpty(rangeHeader)) {
this.state = State.READ_CONTENT;
} else {
ContentRange cr = parseRange(rangeHeader);
if (cr == null) {
this.state = State.READ_CONTENT;
} else {
this.contentRange = cr;
this.state = State.READ_CHUNKD_CONTENT;
}
}
}
static final Pattern ContentRange = Pattern.compile(
"\\s*bytes\\s*([0-9]+)-([0-9]+)/[0-9]+", Pattern.CASE_INSENSITIVE);
protected ContentRange parseRange(String contentRange) {
Matcher m = ContentRange.matcher(contentRange);
if (m.find()) {
String begin = m.group(1);
String end = m.group(2);
if (StringUtil.isEmpty(begin) == false
&& StringUtil.isEmpty(end) == false) {
ContentRange cr = new ContentRange();
cr.length = Long.parseLong(end) - Long.parseLong(begin) + 1;
return cr;
}
}
return null;
}
class ContentRange {
long length = 0L;
long consumed = 0L;
boolean pass(ChannelBuffer buffer) {
this.consumed += buffer.readableBytes();
return this.consumed <= this.length;
}
}
private String readLine(ChannelBuffer buffer) {
StringBuilder sb = new StringBuilder(64);
while (buffer.readable()) {
byte nextByte = buffer.readByte();
if (nextByte == HttpCodecUtil.CR) {
nextByte = buffer.readByte();
if (nextByte == HttpCodecUtil.LF) {
return sb.toString();
}
} else if (nextByte == HttpCodecUtil.LF) {
return sb.toString();
} else {
sb.append((char) nextByte);
}
}
return "";
}
private String[] splitHeader(String sb) {
final int length = sb.length();
int nameStart;
int nameEnd;
int colonEnd;
int valueStart;
int valueEnd;
nameStart = findNonWhitespace(sb, 0);
for (nameEnd = nameStart; nameEnd < length; nameEnd++) {
char ch = sb.charAt(nameEnd);
if (ch == ':' || Character.isWhitespace(ch)) {
break;
}
}
for (colonEnd = nameEnd; colonEnd < length; colonEnd++) {
if (sb.charAt(colonEnd) == ':') {
colonEnd++;
break;
}
}
valueStart = findNonWhitespace(sb, colonEnd);
if (valueStart == length) {
return new String[] { sb.substring(nameStart, nameEnd), "" };
}
valueEnd = findEndOfString(sb);
return new String[] { sb.substring(nameStart, nameEnd),
sb.substring(valueStart, valueEnd) };
}
private int findNonWhitespace(String sb, int offset) {
int result;
for (result = offset; result < sb.length(); result++) {
if (!Character.isWhitespace(sb.charAt(result))) {
break;
}
}
return result;
}
private int findEndOfString(String sb) {
int result;
for (result = sb.length(); result > 0; result--) {
if (!Character.isWhitespace(sb.charAt(result - 1))) {
break;
}
}
return result;
}
private int seekNextBoundary(ChannelBuffer buffer) {
int readerIndex = buffer.readerIndex();
int length = 0;
String line = "";
while (buffer.readable()) {
line = readLine(buffer);
if (line.isEmpty() && buffer.readable() == false) {
this.state = State.READ_CONTENT;
break;
} else if (this.dashBoundary.equals(line)) {
length -= this.dashBoundary.length();
length -= 4; // CRLF x 2
this.state = State.SKIP_CONTROL_CHARS;
break;
} else if (this.closeBoundary.equals(line)) {
length -= this.closeBoundary.length();
length -= 4;
this.state = State.SKIP_CONTROL_CHARS;
break;
}
}
length += buffer.readerIndex() - readerIndex;
if (LOG.isDebugEnabled()) {
LOG.debug(Markers.DETAIL, "content length :" + length + " "
+ this.state);
}
buffer.readerIndex(readerIndex);
return length;
}
}