/*
* Copyright 2012-2015, the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.flipkart.phantom.runtime.impl.server.netty.handler.command;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.flipkart.phantom.task.impl.TaskResult;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferInputStream;
import org.jboss.netty.buffer.ChannelBufferOutputStream;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.springframework.util.SerializationUtils;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* <code>CommandInterpreter</code> interprets a Command from the Netty {@link MessageEvent}
* The command protocol is defined as follows:
*
* <pre>
* Command is described as below
* +------------+---------+------------+--------------+---+---------------+------------+--------------+---+---------------+----+
* | delim char | command | delim char | param name 1 | = | param value 1 | delim char | param name n | = | param value n | \n |
* +------------+---------+------------+--------------+---+---------------+------------+--------------+---+---------------+----+
* +------------+
* | data bytes |
* +------------+
*
* where
* <ul>
* <li>Command and params appear on a single line terminating in '\n' char</li>
* <li>'delim char' is any non-ascii character</li>
* <li>'command' is an arbitrary sequence of characters</li>
* <li>'param name'='param value' can repeat any number of times. Are of type : arbitrary sequence of characters</li>
* <li>'data' is an arbitrary sequence of bytes</li>
* </ul>
*
* Response from Command execution is described as below
*
* +--------+----+
* | status | \n |
* +--------+----+
* (or)
* +--------+-------------+-------------+----+
* | status | white space | data length | \n |
* +--------+-------------+-------------+----+
* +------------+
* | data bytes |
* +------------+
*
* <pre>
*
* Command protocol interpretation code is based on the implementation in com.flipkart.w3.agent.W3Agent
*
* @author Regunath B
* @version 1.0, 22 Mar 2013
*/
@SuppressWarnings("rawtypes")
public class CommandInterpreter {
/** Constant for max command input size*/
public static final int MAX_COMMAND_INPUT = 20480;
/** Constants for characters that have special meaning in the command protocol*/
public static final char LINE_FEED = '\n';
private static final char CARRIAGE_RETURN = '\r';
private static final char DEFAULT_DELIM = ' ';
private static final char PARAM_VALUE_SEP = '=';
private static final char[] ASCII_LOW = {'a','z'};
private static final char[] ASCII_HIGH = {'A','Z'};
private static final String SUCCESS = "SUCCESS";
private static final String ERROR = "ERROR";
private static final String NULL_STRING = "";
/** Default param value, when none is specified*/
private static final String DEFAULT_PARAM_VALUE = "true";
/** The Jackson ObjectMapper for writing output as JSON*/
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); // using an instance variable as this class is deemed to be thread-safe
/** Enumeration of read failure reasons */
public enum ReadFailure {
INSUFFICIENT_DATA,
}
/**
* Helper method to read and return a ProxyCommand from an {@link InputStream}. Throws Exception for all data read errors including partial
* reads arising from insufficient data
* @param inputStream the InputStream instance
* @return the read ProxyCommand
* @throws Exception in case of errors
*/
public ProxyCommand readCommand(InputStream inputStream) throws Exception {
return this.interpretCommand(inputStream, true);
}
/**
* Helper method to read and return a ProxyCommand from an input Channel {@link MessageEvent}. Throws Exception for all data read errors including partial
* reads arising from insufficient data
* @param event the MessageEvent instance
* @return the read ProxyCommand
* @throws Exception in case of errors
*/
public ProxyCommand readCommand(MessageEvent event) throws Exception {
return this.interpretCommand(new ChannelBufferInputStream((ChannelBuffer)event.getMessage()), true);
}
/**
* Helper method to read and return a ProxyCommand from a ChannelBuffer {@link ChannelBuffer}. Returns a ProxyCommand for partial read errors and throws
* Exception only for irrecoverable errors. Useful method to decode data frames from the raw input channel buffer.
* @param buffer the input buffer
* @return the read ProxyCommand
* @throws Exception in case of errors
*/
public ProxyCommand interpretCommand(ChannelBuffer buffer) throws Exception {
return this.interpretCommand(new ChannelBufferInputStream(buffer), false);
}
/**
* Writes the specified TaskResult data to the channel output following the Command protocol
* @param ctx the ChannelHandlerContext
* @param event the ChannelEvent
* @param result the TaskResult data written to the channel response
* @throws Exception in case of any errors
*/
public void writeCommandExecutionResponse(ChannelHandlerContext ctx, ChannelEvent event, TaskResult result) throws Exception {
ChannelBuffer writeBuffer = ChannelBuffers.dynamicBuffer();
this.writeCommandExecutionResponse(new ChannelBufferOutputStream(writeBuffer), result);
Channels.write(ctx, event.getFuture(), writeBuffer);
}
/**
* Writes the specified TaskResult data to the Outputstream following the Command protocol
* @param outputStream the Outputstream to write result data to
* @param result the TaskResult to write
* @throws Exception in case of any errors
*/
public void writeCommandExecutionResponse(OutputStream outputStream, TaskResult result) throws Exception {
//Don't write anything if the result is null
if(result==null) {
return;
}
String message = result.getMessage();
boolean success = result.isSuccess();
int resultDatalength = result.getLength();
String metaContents = (message==null ? (success ? SUCCESS : ERROR) : message);
metaContents += (resultDatalength==0 ? LINE_FEED : (DEFAULT_DELIM + NULL_STRING + resultDatalength + NULL_STRING + LINE_FEED));
// write the meta contents
outputStream.write(metaContents.getBytes());
// now write the result data
if(result.isDataArray()) {
for(Object object : result.getDataArray()) {
if(object!=null) {
if(object instanceof byte[]) {
outputStream.write((byte[]) object);
} else {
outputStream.write(SerializationUtils.serialize(object));
}
}
}
} else {
byte[] metaData = result.getMetadata();
if(metaData != null && (metaData.length > 0)) {
outputStream.write(metaData);
}
Object data = result.getData();
if(data!=null) {
if(data instanceof byte[]) {
byte[] byteData = (byte[]) data;
if(byteData.length>0) {
outputStream.write(byteData);
}
} else {
outputStream.write(SerializationUtils.serialize(data));
}
}
}
}
/**
* Helper method to read and return a ProxyCommand from an input {@link InputStream}
* @param inputStream the InputStream instance
* @param isFramedTransport boolean indicator that defines mechanism for reporting errors - Exceptions vs a ProxyCommand with error description
* @return the read ProxyCommand
* @throws Exception in case of errors
*/
private ProxyCommand interpretCommand(InputStream inputStream, boolean isFramedTransport) throws Exception {
ProxyCommand readCommand = null;
byte[] readBytes = new byte[MAX_COMMAND_INPUT];
int byteReadIndex=0, commandEndIndex=0, dataStartIndex=0, dataLength=0;
while(byteReadIndex < MAX_COMMAND_INPUT) {
int bytesRead = inputStream.read(readBytes, byteReadIndex, MAX_COMMAND_INPUT-byteReadIndex); // try to read as much as is available into the byte array
if(bytesRead <= 0){ // check if no data was read at all. Throw an IllegalArgumentException to indicate unexpected end of stream
if (isFramedTransport) {
throw new IllegalArgumentException("Invalid read. Encountered end of stream before reading a single byte");
} else {
return new ProxyCommand(ReadFailure.INSUFFICIENT_DATA, "Invalid read. Encountered end of stream before reading a single byte");
}
}
for(int i=0; i<bytesRead; i++) { // look for the NEW_LINE character that signals end of command and params input
if(readBytes[byteReadIndex+i ]== LINE_FEED) {
commandEndIndex = byteReadIndex+i;
break;
}
}
if (bytesRead > 0) {
byteReadIndex += bytesRead; // skip the read bytes by moving the index for next read
}
if(commandEndIndex > 0 || bytesRead <= 0) { // break the read loop if end of command line is reached (or) EOS (end of stream is reached)
break; // i.e. no more data available for read
}
}
if(commandEndIndex==0) { // report a suitable error if NEW_LINE was not encountered at all (or) if bytes read has exceeded MAX_COMMAND_INPUT
if(byteReadIndex < MAX_COMMAND_INPUT){
if (isFramedTransport) {
throw new IllegalArgumentException("Stream ended before encountering a \\n: " + new String(readBytes,0,byteReadIndex));
} else {
return new ProxyCommand(ReadFailure.INSUFFICIENT_DATA, "Stream ended before encountering a \\n: " + new String(readBytes,0,byteReadIndex));
}
} else {
throw new IllegalArgumentException("Maximum command line size allowed: " + MAX_COMMAND_INPUT +" Command : "+ new String(readBytes,0,byteReadIndex));
}
}
// The input data appears to adhere to the command protocol. Proceed to read the command, params and data
dataStartIndex = commandEndIndex+1;
if (readBytes[commandEndIndex-1] == CARRIAGE_RETURN) {
commandEndIndex--; // handle the CR for people who still haven't moved on from telnet to netcat
}
byte delimiter = DEFAULT_DELIM;
int fragmentStart = 0;
if(!(readBytes[0]>=ASCII_LOW[0] && readBytes[0]<=ASCII_LOW[1]) && !(readBytes[0]>=ASCII_HIGH[0] && readBytes[0]<=ASCII_HIGH[1])) {
delimiter = readBytes[0]; // the delimiter is not DEFAULT_DELIM but the non-ascii character appearing as the first byte
fragmentStart=1;
}
int fragmentIndex = this.getNextCommandFragmentPosition(readBytes, fragmentStart, commandEndIndex, delimiter);
readCommand = new ProxyCommand(new String(readBytes, fragmentStart, fragmentIndex-fragmentStart));
Map<String,String> commandParams = new HashMap<String, String>();
// gather params
while(fragmentIndex < commandEndIndex) {
// skip initial delims
while(fragmentIndex < commandEndIndex && readBytes[fragmentIndex] == delimiter) {
fragmentIndex++;
}
if (fragmentIndex == commandEndIndex) {
break;
}
// read first char
if(Character.isDigit((char)readBytes[fragmentIndex])) {
// this is the datalen
try {
dataLength = Integer.parseInt(new String(readBytes, fragmentIndex, commandEndIndex-fragmentIndex));
break;
} catch (Exception e) {
throw new IllegalArgumentException("Invalid syntax in command: "+new String(readBytes), e);
}
} else {
fragmentStart = fragmentIndex;
fragmentIndex = getNextCommandFragmentPosition(readBytes, fragmentIndex+1, commandEndIndex, delimiter);
int paramValueSepIndex = 0;
for(int i=fragmentStart; i<fragmentIndex; i++) {
if (readBytes[i] == PARAM_VALUE_SEP) {
paramValueSepIndex = i;
break;
}
}
if (paramValueSepIndex > 0) {
commandParams.put(new String(readBytes, fragmentStart, paramValueSepIndex-fragmentStart),
new String(readBytes, paramValueSepIndex+1, fragmentIndex-paramValueSepIndex-1));
} else {
commandParams.put(new String(readBytes, fragmentStart, fragmentIndex-fragmentStart), DEFAULT_PARAM_VALUE); // initialize with default value if none specified
}
// set the params on the ProxyCommand object
readCommand.setCommandParams(commandParams);
}
}
if(dataLength > 0) {
byte[] commandData = new byte[dataLength];
int dataByteReadIndex = byteReadIndex-dataStartIndex;
if(dataStartIndex < byteReadIndex){
System.arraycopy(readBytes, dataStartIndex, commandData, 0, dataByteReadIndex);
}
while(dataByteReadIndex<dataLength){
if (inputStream.available() < (dataLength-dataByteReadIndex)) {
if (!isFramedTransport) { // check if all data bytes have been received. Return immediately for non framed transports
return new ProxyCommand(ReadFailure.INSUFFICIENT_DATA, "Stream ended before all data was read. Length of data bytes needed : " + (dataLength-dataByteReadIndex));
}
}
int actualBytesRead = inputStream.read(commandData, dataByteReadIndex, dataLength-dataByteReadIndex);
if (actualBytesRead <= 0) { // 0 bytes not possible because dataLength-dataByteReadIndex is non-zero, -1 is returned if no byte is available because the stream is at end of file (as per Javadocs)
throw new IllegalArgumentException("Insufficient bytes read for command : " + readCommand.getCommand() + ". Expected : " + (dataLength-dataByteReadIndex) + " but read : " + actualBytesRead);
}
dataByteReadIndex += actualBytesRead;
}
// set the command data on the ProxyCommand object
readCommand.setCommandData(commandData);
}
return readCommand;
}
/**
* Helper method to return the next command fragment position in the input byte array. Considers the start index to skip bytes and the delim char to
* identify the next fragment
* @return the start position of the next command fragment
*/
private int getNextCommandFragmentPosition(byte[] arr, int fragmentStart, int lastPos, byte delim) {
for(; fragmentStart<lastPos; fragmentStart++) {
if(arr[fragmentStart]==delim) {
return fragmentStart;
}
}
return fragmentStart;
}
/**
* Helper class to store command protocol objects
*/
public class ProxyCommand {
/** The command String*/
private String command;
/** The read failure reason and message*/
private ReadFailure readFailure;
private String readFailureDescription;
/** The command parameters*/
private Map<String, String> commandParams = new HashMap<String, String>();
/** The command data*/
private byte[] commandData;
/**
* Constructor for this class
* @param command the command string
*/
public ProxyCommand(String command) {
this.command = command;
}
/**
* Constructor for this class
* @param readFailure the ReadFailure reason
* @param readFailureDescription the error description
*/
public ProxyCommand(ReadFailure readFailure, String readFailureDescription) {
this.readFailure = readFailure;
this.readFailureDescription = readFailureDescription;
}
/**
* Overriden super class method. Returns a string representation of this ProxyCommand
* @see java.lang.Object#toString()
*/
public String toString() {
try {
return String.format("ProxyCommand[Command = %s, Read Error = %s, Params = %s" + "]",this.getCommand(), this.getReadFailureDescription(),
commandParams != null ? OBJECT_MAPPER.writeValueAsString(this.getCommandParams()) : "");
} catch (Exception e) {
// ignore JSON formating errors and return just the command string
return "ProxyCommand[Command = " + command + ". Read Error = " + readFailureDescription + "]";
}
}
/** Start setter/getter methods*/
public String getCommand() {
return command;
}
public ReadFailure getReadFailure() {
return readFailure;
}
public String getReadFailureDescription() {
return readFailureDescription;
}
public Map<String, String> getCommandParams() {
return commandParams;
}
public void setCommandParams(Map<String, String> commandParams) {
this.commandParams = commandParams;
}
public byte[] getCommandData() {
return this.commandData;
}
public void setCommandData(byte[] commandData) {
this.commandData = commandData;
}
/** End setter/getter methods*/
}
}