package voldemort.rest;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LOCATION;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TRANSFER_ENCODING;
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 java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.log4j.Logger;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponse;
import voldemort.store.stats.StoreStats;
import voldemort.store.stats.Tracked;
import voldemort.utils.ByteArray;
import voldemort.versioning.VectorClock;
import voldemort.versioning.Versioned;
public class GetAllResponseSender extends RestResponseSender {
private Map<ByteArray, List<Versioned<byte[]>>> versionedResponses;
private String storeName;
private static final Logger logger = Logger.getLogger(GetAllResponseSender.class);
public GetAllResponseSender(MessageEvent messageEvent,
Map<ByteArray, List<Versioned<byte[]>>> versionedResponses,
String storeName) {
super(messageEvent);
this.versionedResponses = versionedResponses;
this.storeName = storeName;
}
/**
* Sends nested multipart response. Outer multipart wraps all the keys
* requested. Each key has a separate multipart for the versioned values.
*/
@Override
public void sendResponse(StoreStats performanceStats,
boolean isFromLocalZone,
long startTimeInMs) throws Exception {
/*
* Pay attention to the code below. Note that in this method we wrap a multiPart object with a mimeMessage.
* However when writing to the outputStream we only send the multiPart object and not the entire
* mimeMessage. This is intentional.
*
* In the earlier version of this code we used to create a multiPart object and just send that multiPart
* across the wire.
*
* However, we later discovered that upon setting the content of a MimeBodyPart, JavaMail internally creates
* a DataHandler object wrapping the object you passed in. The part's Content-Type header is not updated
* immediately. In order to get the headers updated, one needs to to call MimeMessage.saveChanges() on the
* enclosing message, which cascades down the MIME structure into a call to MimeBodyPart.updateHeaders()
* on the body part. It's this updateHeaders call that transfers the content type from the
* DataHandler to the part's MIME Content-Type header.
*
* To make sure that the Content-Type headers are being updated (without changing too much code), we decided
* to wrap the multiPart in a mimeMessage, call mimeMessage.saveChanges() and then just send the multiPart.
* This is to make sure multiPart's headers are updated accurately.
*/
MimeMessage message = new MimeMessage(Session.getDefaultInstance(new Properties()));
// multiPartKeys is the outer multipart
MimeMultipart multiPartKeys = new MimeMultipart();
ByteArrayOutputStream keysOutputStream = new ByteArrayOutputStream();
for(Entry<ByteArray, List<Versioned<byte[]>>> entry: versionedResponses.entrySet()) {
ByteArray key = entry.getKey();
String base64Key = RestUtils.encodeVoldemortKey(key.get());
String contentLocationKey = "/" + this.storeName + "/" + base64Key;
// Create the individual body part - for each key requested
MimeBodyPart keyBody = new MimeBodyPart();
try {
// Add the right headers
keyBody.addHeader(CONTENT_TYPE, "application/octet-stream");
keyBody.addHeader(CONTENT_TRANSFER_ENCODING, "binary");
keyBody.addHeader(CONTENT_LOCATION, contentLocationKey);
} catch(MessagingException me) {
logger.error("Exception while constructing key body headers", me);
keysOutputStream.close();
throw me;
}
// multiPartValues is the inner multipart
MimeMultipart multiPartValues = new MimeMultipart();
for(Versioned<byte[]> versionedValue: entry.getValue()) {
byte[] responseValue = versionedValue.getValue();
VectorClock vectorClock = (VectorClock) versionedValue.getVersion();
String eTag = RestUtils.getSerializedVectorClock(vectorClock);
numVectorClockEntries += vectorClock.getVersionMap().size();
// Create the individual body part - for each versioned value of
// a key
MimeBodyPart valueBody = new MimeBodyPart();
try {
// Add the right headers
valueBody.addHeader(CONTENT_TYPE, "application/octet-stream");
valueBody.addHeader(CONTENT_TRANSFER_ENCODING, "binary");
valueBody.addHeader(RestMessageHeaders.X_VOLD_VECTOR_CLOCK, eTag);
valueBody.setContent(responseValue, "application/octet-stream");
valueBody.addHeader(RestMessageHeaders.CONTENT_LENGTH,
Integer.toString(responseValue.length));
multiPartValues.addBodyPart(valueBody);
} catch(MessagingException me) {
logger.error("Exception while constructing value body part", me);
keysOutputStream.close();
throw me;
}
}
try {
// Add the inner multipart as the content of the outer body part
keyBody.setContent(multiPartValues);
multiPartKeys.addBodyPart(keyBody);
} catch(MessagingException me) {
logger.error("Exception while constructing key body part", me);
keysOutputStream.close();
throw me;
}
}
message.setContent(multiPartKeys);
message.saveChanges();
try {
multiPartKeys.writeTo(keysOutputStream);
} catch(Exception e) {
logger.error("Exception while writing mutipart to output stream", e);
throw e;
}
ChannelBuffer responseContent = ChannelBuffers.dynamicBuffer();
responseContent.writeBytes(keysOutputStream.toByteArray());
// Create the Response object
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
// Set the right headers
response.setHeader(CONTENT_TYPE, "multipart/binary");
response.setHeader(CONTENT_TRANSFER_ENCODING, "binary");
// Copy the data into the payload
response.setContent(responseContent);
response.setHeader(CONTENT_LENGTH, response.getContent().readableBytes());
// Write the response to the Netty Channel
if(logger.isDebugEnabled()) {
String keyStr = getKeysHexString(this.versionedResponses.keySet());
debugLog("GET_ALL",
this.storeName,
keyStr,
startTimeInMs,
System.currentTimeMillis(),
numVectorClockEntries);
}
this.messageEvent.getChannel().write(response);
if(performanceStats != null && isFromLocalZone) {
recordStats(performanceStats, startTimeInMs, Tracked.GET_ALL);
}
keysOutputStream.close();
}
}