/*
* 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 org.f1x.io.parsers;
import org.f1x.api.FixParserException;
import org.f1x.util.AsciiUtils;
/**
* Micro FIX parser. Thread-safe.
*
* Override {@link #onTagNumber(int, Object)} and {@link #onTagValue(int, byte[], int, int, Object)} to get callback on each tag and value.
*/
public abstract class SimpleMessageScanner<Cookie> {
public static final int MIN_MESSAGE_LENGTH = 63; // min message example: 8=FIX.4.?|9=??|35=?|34=?|49=?|56=?|52=YYYYMMDD-HH:MM:SS|10=???|
private static final byte [] HEADER_START = AsciiUtils.getBytes("8=FIX.");
private static final int HEADER_START_LENGTH = HEADER_START.length;
private static final byte SOH = 1;
private static final byte EQUALS = '=';
private static final int BODY_LENGTH_TAG_NUM = 9;
private static final int CHECKSUM_LENGTH = 8; // including SOH
/**
* Tries to parse single FIX message in given byte buffer.
*
* @return Result indicates one of the following.
* Negative value indicates that supplied buffer does not contain whole message (in which case result is the number of missing bytes).
* Positive value indicates that a single message is completely parsed (in which case result is offset of the first byte after the message).
*/
public int parse (byte [] buffer, int offset, int len, Cookie cookie) throws MessageFormatException {
if (len < MIN_MESSAGE_LENGTH)
throw MessageFormatException.BUFFER_IS_TOO_SMALL;
if ( ! AsciiUtils.equals(HEADER_START, buffer, offset, HEADER_START_LENGTH))
throw MessageFormatException.NOT_FIX_MESSAGE;
int end = len + offset;
// Skip FIX version
offset += HEADER_START_LENGTH + 4; // X.X|
int bodyLength = 0;
int currentTagNum = 0;
int currentTagValueStart = 0;
boolean parsingTagNum = true;
for (int i=offset; i < end; i++) {
byte b = buffer[i];
if (parsingTagNum) {
if (b == EQUALS) {
if (onTagNumber(currentTagNum, cookie)) {
currentTagValueStart = i+1;
}
parsingTagNum = false;
} else {
currentTagNum = 10*currentTagNum + parseDigit(b);
}
} else {
if (b == SOH) {
if (currentTagValueStart > 0) {
if ( ! onTagValue(currentTagNum, buffer, currentTagValueStart, i - currentTagValueStart, cookie))
break;
currentTagValueStart = -1;
}
if (currentTagNum == BODY_LENGTH_TAG_NUM) {
// number of characters in the message following the BodyLength field up to, and including, the delimiter immediately preceding the CheckSum tag ("10=")
int messageBodyEnd = bodyLength + i + CHECKSUM_LENGTH; // index of first byte beyond message end
if (end < messageBodyEnd)
return - (messageBodyEnd - end);
else
end = messageBodyEnd;
}
parsingTagNum = true;
currentTagNum = 0;
} else {
if (currentTagNum == BODY_LENGTH_TAG_NUM) {
bodyLength = 10*bodyLength + parseDigit(b);
}
}
}
}
if (bodyLength == 0)
throw MessageFormatException.MISSING_BODY_LEN;
else
return end;
}
/** @return true to process value of this tag (get onTagValue callaback) or false to skip this tag */
protected abstract boolean onTagNumber(int tagNum, Cookie cookie) throws FixParserException;
/** @return true to continue parsing message or false to skip the rest tags */
protected abstract boolean onTagValue(int tagNum, byte [] message, int tagValueStart, int tagValueLen, Cookie cookie) throws FixParserException;
private static int parseDigit(byte b) throws MessageFormatException {
int digit = b - '0';
if (digit < 0 || digit > 9)
throw MessageFormatException.INVALID_NUMBER;
return digit;
}
public static class MessageFormatException extends FixParserException {
public static final MessageFormatException BUFFER_IS_TOO_SMALL = new MessageFormatException("Message is too small");
public static final MessageFormatException INVALID_NUMBER = new MessageFormatException("Expecting a number");
public static final MessageFormatException NOT_FIX_MESSAGE = new MessageFormatException("Not a FIX message");
public static final MessageFormatException MISSING_BODY_LEN = new MessageFormatException("Missing BodyLength(9) tag");
private MessageFormatException(String message) {
super(message);
}
@Override
public Throwable fillInStackTrace () {
return null;
}
}
}