package voldemort.rest;
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.codehaus.jackson.map.ObjectMapper;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import voldemort.VoldemortException;
import voldemort.server.RequestRoutingType;
import voldemort.store.CompositeVoldemortRequest;
import voldemort.utils.ByteArray;
import voldemort.versioning.VectorClock;
/**
* Super class to parse, validate REST requests
*/
public abstract class RestRequestValidator {
protected CompositeVoldemortRequest<ByteArray, byte[]> requestObject;
protected String storeName = null;
protected List<ByteArray> parsedKeys;
protected byte[] parsedValue = null;
protected VectorClock parsedVectorClock = null;
protected long parsedTimeoutInMs;
protected byte parsedOperationType;
protected long parsedRequestOriginTimeInMs;
protected RequestRoutingType parsedRoutingType = null;
protected MessageEvent messageEvent;
protected HttpRequest request;
protected final Logger logger = Logger.getLogger(getClass());
public RestRequestValidator(HttpRequest request, MessageEvent messageEvent) {
this.request = request;
this.messageEvent = messageEvent;
this.parsedRoutingType = RequestRoutingType.NORMAL;
}
public abstract CompositeVoldemortRequest<ByteArray, byte[]> constructCompositeVoldemortRequestObject();
/**
* Validations common for all operations are done here
*
* @return true if request is valid else false
*/
protected boolean parseAndValidateRequest() {
if(!hasKey() || !hasTimeOutHeader() || !hasTimeStampHeader() || !isStoreValid()) {
return false;
}
// Retrieve the routing code from the header
parseRoutingCodeHeader();
return true;
}
/**
* Retrieve and validate the timeout value from the REST request.
* "X_VOLD_REQUEST_TIMEOUT_MS" is the timeout header.
*
* @return true if present, false if missing
*/
protected boolean hasTimeOutHeader() {
boolean result = false;
String timeoutValStr = this.request.getHeader(RestMessageHeaders.X_VOLD_REQUEST_TIMEOUT_MS);
if(timeoutValStr != null) {
try {
this.parsedTimeoutInMs = Long.parseLong(timeoutValStr);
if(this.parsedTimeoutInMs < 0) {
RestErrorHandler.writeErrorResponse(messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Time out cannot be negative ");
} else {
result = true;
}
} catch(NumberFormatException nfe) {
logger.error("Exception when validating request. Incorrect timeout parameter. Cannot parse this to long: "
+ timeoutValStr,
nfe);
RestErrorHandler.writeErrorResponse(this.messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Incorrect timeout parameter. Cannot parse this to long: "
+ timeoutValStr);
}
} else {
logger.error("Error when validating request. Missing timeout parameter.");
RestErrorHandler.writeErrorResponse(this.messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Missing timeout parameter.");
}
return result;
}
/**
* Retrieve the routing type value from the REST request.
* "X_VOLD_ROUTING_TYPE_CODE" is the routing type header.
*
* By default, the routing code is set to NORMAL
*
* TODO REST-Server 1. Change the header name to a better name. 2. Assumes
* that integer is passed in the header
*
*/
protected void parseRoutingCodeHeader() {
String rtCode = this.request.getHeader(RestMessageHeaders.X_VOLD_ROUTING_TYPE_CODE);
if(rtCode != null) {
try {
int routingTypeCode = Integer.parseInt(rtCode);
this.parsedRoutingType = RequestRoutingType.getRequestRoutingType(routingTypeCode);
} catch(NumberFormatException nfe) {
logger.error("Exception when validating request. Incorrect routing type parameter. Cannot parse this to long: "
+ rtCode,
nfe);
RestErrorHandler.writeErrorResponse(this.messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Incorrect routing type parameter. Cannot parse this to long: "
+ rtCode);
} catch(VoldemortException ve) {
logger.error("Exception when validating request. Incorrect routing type code: "
+ rtCode, ve);
RestErrorHandler.writeErrorResponse(this.messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Incorrect routing type code: " + rtCode);
}
}
}
/**
* Retrieve and validate the timestamp value from the REST request.
* "X_VOLD_REQUEST_ORIGIN_TIME_MS" is timestamp header
*
* TODO REST-Server 1. Change Time stamp header to a better name.
*
* @return true if present, false if missing
*/
protected boolean hasTimeStampHeader() {
String originTime = request.getHeader(RestMessageHeaders.X_VOLD_REQUEST_ORIGIN_TIME_MS);
boolean result = false;
if(originTime != null) {
try {
// TODO: remove the originTime field from request header,
// because coordinator should not accept the request origin time
// from the client.. In this commit, we only changed
// "this.parsedRequestOriginTimeInMs" from
// "Long.parseLong(originTime)" to current system time,
// The reason that we did not remove the field from request
// header right now, is because this commit is a quick fix for
// internal performance test to be available as soon as
// possible.
this.parsedRequestOriginTimeInMs = System.currentTimeMillis();
if(this.parsedRequestOriginTimeInMs < 0) {
RestErrorHandler.writeErrorResponse(messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Origin time cannot be negative ");
} else {
result = true;
}
} catch(NumberFormatException nfe) {
logger.error("Exception when validating request. Incorrect origin time parameter. Cannot parse this to long: "
+ originTime,
nfe);
RestErrorHandler.writeErrorResponse(this.messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Incorrect origin time parameter. Cannot parse this to long: "
+ originTime);
}
} else {
logger.error("Error when validating request. Missing origin time parameter.");
RestErrorHandler.writeErrorResponse(this.messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Missing origin time parameter.");
}
return result;
}
/**
* Retrieve and validate vector clock value from the REST request.
* "X_VOLD_VECTOR_CLOCK" is the vector clock header.
*
* @return true if present, false if missing
*/
protected boolean hasVectorClock(boolean isVectorClockOptional) {
boolean result = false;
String vectorClockHeader = this.request.getHeader(RestMessageHeaders.X_VOLD_VECTOR_CLOCK);
if(vectorClockHeader != null) {
ObjectMapper mapper = new ObjectMapper();
try {
VectorClockWrapper vcWrapper = mapper.readValue(vectorClockHeader,
VectorClockWrapper.class);
this.parsedVectorClock = new VectorClock(vcWrapper.getVersions(),
vcWrapper.getTimestamp());
result = true;
} catch(Exception e) {
logger.error("Exception while parsing and constructing vector clock", e);
RestErrorHandler.writeErrorResponse(this.messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Invalid Vector Clock");
}
} else if(!isVectorClockOptional) {
logger.error("Error when validating request. Missing Vector Clock");
RestErrorHandler.writeErrorResponse(this.messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Missing Vector Clock");
} else {
result = true;
}
return result;
}
/**
* Retrieve and validate the key from the REST request.
*
* @return true if present, false if missing
*/
protected boolean hasKey() {
boolean result = false;
String requestURI = this.request.getUri();
parseKeys(requestURI);
if(this.parsedKeys != null) {
result = true;
} else {
logger.error("Error when validating request. No key specified.");
RestErrorHandler.writeErrorResponse(this.messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Error: No key specified !");
}
return result;
}
/**
* Method to read a key (or keys) present in the HTTP request URI. The URI
* must be of the format /<store_name>/<key>[,<key>,...]
*
* @param requestURI The URI of the HTTP request
*/
protected void parseKeys(String requestURI) {
this.parsedKeys = null;
String[] parts = requestURI.split("/");
if(parts.length > 2) {
String base64KeyList = parts[2];
this.parsedKeys = new ArrayList<ByteArray>();
if(!base64KeyList.contains(",")) {
String rawKey = base64KeyList.trim();
this.parsedKeys.add(new ByteArray(RestUtils.decodeVoldemortKey(rawKey)));
} else {
String[] base64KeyArray = base64KeyList.split(",");
for(String base64Key: base64KeyArray) {
String rawKey = base64Key.trim();
this.parsedKeys.add(new ByteArray(RestUtils.decodeVoldemortKey(rawKey)));
}
}
}
}
/**
* Retrieve and validate store name from the REST request.
*
* @return true if valid, false otherwise
*/
protected boolean isStoreValid() {
boolean result = false;
String requestURI = this.request.getUri();
this.storeName = parseStoreName(requestURI);
if(storeName != null) {
result = true;
} else {
logger.error("Error when validatig request. Missing store name.");
RestErrorHandler.writeErrorResponse(this.messageEvent,
HttpResponseStatus.BAD_REQUEST,
"Missing store name. Critical error.");
}
return result;
}
/**
* Parses the store name HTTP request URI. The URI must be of the format
* /<store_name>/<key>[,<key>,...]
*
* @param requestURI
* @return a String representing store name
*/
protected String parseStoreName(String requestURI) {
String storeName = null;
String[] parts = requestURI.split("/");
if(parts.length > 1) {
storeName = parts[1];
}
return storeName;
}
public String getStoreName() {
return this.storeName;
}
public RequestRoutingType getParsedRoutingType() {
return parsedRoutingType;
}
/**
* Prints a debug log message that details the time taken for the Http
* request to be parsed by the coordinator
*
* @param operationType
* @param receivedTimeInMs
*/
protected void debugLog(String operationType, Long receivedTimeInMs) {
long durationInMs = receivedTimeInMs - (this.parsedRequestOriginTimeInMs);
int numVectorClockEntries = (this.parsedVectorClock == null ? 0
: this.parsedVectorClock.getVersionMap()
.size());
logger.debug("Received a new request. Operation type: " + operationType + " , Key(s): "
+ keysHexString(this.parsedKeys) + " , Store: " + this.storeName
+ " , Origin time (in ms): " + (this.parsedRequestOriginTimeInMs)
+ " , Request received at time(in ms): " + receivedTimeInMs
+ " , Num vector clock entries: " + numVectorClockEntries
+ " , Duration from RESTClient to CoordinatorRestRequestValidator(in ms): "
+ durationInMs);
}
protected String keysHexString(List<ByteArray> keys) {
return RestUtils.getKeysHexString(keys.iterator());
}
}