All Downloads are FREE. Search and download functionalities are using the official Maven repository.

quickfix.mina.message.FIXMessageDecoder Maven / Gradle / Ivy

There is a newer version: 2.3.1
Show newest version
/*******************************************************************************
 * Copyright (c) quickfixengine.org  All rights reserved.
 *
 * This file is part of the QuickFIX FIX Engine
 *
 * This file may be distributed under the terms of the quickfixengine.org
 * license as defined by quickfixengine.org and appearing in the file
 * LICENSE included in the packaging of this file.
 *
 * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
 * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE.
 *
 * See http://www.quickfixengine.org/LICENSE for licensing information.
 *
 * Contact [email protected] if any conditions of this licensing
 * are not clear to you.
 ******************************************************************************/

package quickfix.mina.message;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.filterchain.IoFilter;
import org.apache.mina.filter.codec.ProtocolCodecException;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.demux.MessageDecoder;
import org.apache.mina.filter.codec.demux.MessageDecoderResult;
import org.quickfixj.CharsetSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import quickfix.mina.CriticalProtocolCodecException;

/**
 * Detects and decodes FIX message strings in an incoming data stream. The
 * message string is then passed to MINA IO handlers for further processing.
 */
public class FIXMessageDecoder implements MessageDecoder {
    private static final char SOH = '\001';
    private static final String FIELD_DELIMITER = String.valueOf(SOH);

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final byte[] HEADER_PATTERN;
    private final byte[] CHECKSUM_PATTERN;
    private final byte[] LOGON_PATTERN;

    // Parsing states
    private static final int SEEKING_HEADER = 1;
    private static final int PARSING_LENGTH = 2;
    private static final int READING_BODY = 3;
    private static final int PARSING_CHECKSUM = 4;

    // If QFJ receives more garbage data than this between messages, then
    // the connection is considered corrupt.
    private static final int MAX_UNDECODED_DATA_LENGTH = 4096;

    private int state;
    private int bodyLength;
    private int position;
    private final String charsetEncoding;

    static class BufPos {
        final int _offset;
        final int _length;

        /**
         * @param offset
         * @param length
         */
        public BufPos(int offset, int length) {
            _offset = offset;
            _length = length;
        }

        public String toString() {
            return _offset + "," + _length;
        }
    }

    private void resetState() {
        state = SEEKING_HEADER;
        bodyLength = 0;
        position = 0;
    }

    public FIXMessageDecoder() throws UnsupportedEncodingException {
        this(CharsetSupport.getCharset(), FIELD_DELIMITER);
    }

    public FIXMessageDecoder(String charset) throws UnsupportedEncodingException {
        this(charset, FIELD_DELIMITER);
    }

    public FIXMessageDecoder(String charset, String delimiter) throws UnsupportedEncodingException {
        charsetEncoding = CharsetSupport.validate(charset);
        HEADER_PATTERN = getBytes("8=FIXt.?.?" + delimiter + "9=");
        CHECKSUM_PATTERN = getBytes("10=???" + delimiter);
        LOGON_PATTERN = getBytes("\00135=A" + delimiter);
        resetState();
    }

    public MessageDecoderResult decodable(IoSession session, IoBuffer in) {
        BufPos bufPos = indexOf(in, in.position(), HEADER_PATTERN);
        int headerOffset = bufPos._offset;
        return headerOffset != -1 ? MessageDecoderResult.OK :
            (in.remaining() > MAX_UNDECODED_DATA_LENGTH ? MessageDecoderResult.NOT_OK : MessageDecoderResult.NEED_DATA);
    }

    public MessageDecoderResult decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out)
            throws ProtocolCodecException {
        int messageCount = 0;
        while (parseMessage(in, out)) {
            messageCount++;
        }
        if (messageCount > 0) {
            // Mina will compact the buffer because we can't detect a header
            if (state == SEEKING_HEADER) {
                position = 0;
            }
            return MessageDecoderResult.OK;
        } else {
            // Mina will compact the buffer
            position -= in.position();
            return MessageDecoderResult.NEED_DATA;
        }
    }

    /**
     * This method cannot move the buffer position until a message is found or an
     * error has occurred. Otherwise, MINA will compact the buffer and we lose
     * data.
     */
    private boolean parseMessage(IoBuffer in, ProtocolDecoderOutput out)
            throws ProtocolCodecException {
        try {
            boolean messageFound = false;
            while (in.hasRemaining() && !messageFound) {
                if (state == SEEKING_HEADER) {

                    BufPos bufPos = indexOf(in, position, HEADER_PATTERN);
                    int headerOffset = bufPos._offset;
                    if (headerOffset == -1) {
                        break;
                    }
                    in.position(headerOffset);

                    if (log.isDebugEnabled()) {
                        log.debug("detected header: " + getBufferDebugInfo(in));
                    }

                    position = headerOffset + bufPos._length;
                    state = PARSING_LENGTH;
                }

                if (state == PARSING_LENGTH) {
                    byte ch = 0;
                    while (hasRemaining(in)) {
                        ch = get(in);
                        if (!Character.isDigit((char) ch)) {
                            break;
                        }
                        bodyLength = bodyLength * 10 + (ch - '0');
                    }
                    if (ch == SOH) {
                        state = READING_BODY;
                        if (log.isDebugEnabled()) {
                            log.debug("body length = " + bodyLength + ": " + getBufferDebugInfo(in));
                        }
                    } else {
                        if (hasRemaining(in)) {
                            String messageString = getMessageStringForError(in);
                            handleError(in, in.position() + 1, "Length format error in message (last character:" + ch + "): " + messageString,
                                    false);
                            continue;
                        } else {
                            break;
                        }
                    }
                }

                if (state == READING_BODY) {
                    if (remaining(in) < bodyLength) {
                        break;
                    }
                    position += bodyLength;
                    state = PARSING_CHECKSUM;
                    if (log.isDebugEnabled()) {
                        log.debug("message body found: " + getBufferDebugInfo(in));
                    }
                }

                if (state == PARSING_CHECKSUM) {
                    if (startsWith(in, position, CHECKSUM_PATTERN) > 0) {
                        // we are trying to parse the checksum but should
                        // check if the CHECKSUM_PATTERN is preceded by SOH
                        // or if the pattern just occurs inside of another field
                        if (in.get(position - 1) != SOH) {
                            handleError(in, position,
                                    "checksum field not preceded by SOH, bad length?", isLogon(in));
                            continue;
                        }
                        if (log.isDebugEnabled()) {
                            log.debug("found checksum: " + getBufferDebugInfo(in));
                        }
                        position += CHECKSUM_PATTERN.length;
                    } else {
                        if (position + CHECKSUM_PATTERN.length <= in.limit()) {
                            // FEATURE allow configurable recovery position
                            // int recoveryPosition = in.position() + 1;
                            // Following recovery position is compatible with QuickFIX C++
                            // but drops messages unnecessarily in corruption scenarios.
                            int recoveryPosition = position + 1;
                            handleError(in, recoveryPosition,
                                    "did not find checksum field, bad length?", isLogon(in));
                            continue;
                        } else {
                            break;
                        }
                    }
                    String messageString = getMessageString(in);
                    if (log.isDebugEnabled()) {
                        log.debug("parsed message: " + getBufferDebugInfo(in) + " " + messageString);
                    }
                    out.write(messageString);
                    state = SEEKING_HEADER;
                    bodyLength = 0;
                    messageFound = true;
                }
            }
            return messageFound;
        } catch (Throwable t) {
            state = SEEKING_HEADER;
            position = 0;
            bodyLength = 0;
            if (t instanceof ProtocolCodecException) {
                throw (ProtocolCodecException) t;
            } else {
                throw new ProtocolCodecException(t);
            }
        }
    }

    private int remaining(IoBuffer in) {
        return in.limit() - position;
    }

    private String getBufferDebugInfo(IoBuffer in) {
        return "pos=" + in.position() + ",lim=" + in.limit() + ",rem=" + in.remaining()
                + ",offset=" + position + ",state=" + state;
    }

    private byte get(IoBuffer in) {
        return in.get(position++);
    }

    private boolean hasRemaining(IoBuffer in) {
        return position < in.limit();
    }

    private static int minMaskLength(byte[] data) {
        int len = 0;
        for (byte aChar : data) {
            if (Character.isLetter(aChar) && Character.isLowerCase(aChar))
                continue;
            ++len;
        }
        return len;
    }

    private String getMessageString(IoBuffer buffer) throws UnsupportedEncodingException {
        byte[] data = new byte[position - buffer.position()];
        buffer.get(data);
        return new String(data, charsetEncoding);
    }

    private String getMessageStringForError(IoBuffer buffer) throws UnsupportedEncodingException {
        int initialPosition = buffer.position();
        byte[] data = new byte[buffer.limit() - initialPosition];
        buffer.get(data);
        buffer.position(position - initialPosition);
        return new String(data, charsetEncoding);
    }

    private void handleError(IoBuffer buffer, int recoveryPosition, String text,
            boolean disconnect) throws ProtocolCodecException {
        buffer.position(recoveryPosition);
        position = recoveryPosition;
        state = SEEKING_HEADER;
        bodyLength = 0;
        if (disconnect) {
            throw new CriticalProtocolCodecException(text);
        } else {
            log.error(text);
        }
    }

    private boolean isLogon(IoBuffer buffer) {
        BufPos bufPos = indexOf(buffer, buffer.position(), LOGON_PATTERN);
        return bufPos._offset != -1;
    }

    private static BufPos indexOf(IoBuffer buffer, int position, byte[] data) {
        for (int offset = position, limit = buffer.limit() - minMaskLength(data) + 1; offset < limit; offset++) {
            int length;
            if (buffer.get(offset) == data[0] && (length = startsWith(buffer, offset, data)) > 0) {
                return new BufPos(offset, length);
            }
        }
        return new BufPos(-1, 0);
    }

    /**
     * Checks to see if the byte_buffer[buffer_offset] starts with data[]. The
     * character ? is a one byte wildcard, lowercase letters are optional.
     *
     * @param buffer
     * @param bufferOffset
     * @param data
     * @return
     */
    private static int startsWith(IoBuffer buffer, int bufferOffset, byte[] data) {
        if (bufferOffset + minMaskLength(data) > buffer.limit()) {
            return -1;
        }
        final int initOffset = bufferOffset;
        int dataOffset = 0;
        for (int bufferLimit = buffer.limit(); dataOffset < data.length
                && bufferOffset < bufferLimit; dataOffset++, bufferOffset++) {
            if (buffer.get(bufferOffset) != data[dataOffset] && data[dataOffset] != '?') {
                // Now check for optional characters, at this point we know we didn't
                // match, so we can just check to see if we failed a match on an optional character,
                // and if so then just rewind the buffer one byte and keep going.
                if (Character.toUpperCase(data[dataOffset]) == buffer.get(bufferOffset))
                    continue;
                // Didn't match the optional character, so act like it was not included and keep going
                if (Character.isLetter(data[dataOffset]) && Character.isLowerCase(data[dataOffset])) {
                    --bufferOffset;
                    continue;
                }
                return -1;
            }
        }
        if (dataOffset != data.length) {
            // when minMaskLength(data) != data.length we might run out of buffer before we run out of data
            return -1;
        }
        return bufferOffset - initOffset;
    }

    public void finishDecode(IoSession arg0, ProtocolDecoderOutput arg1) throws Exception {
        // empty
    }

    /**
     * Used to process streamed messages from a file
     */
    public interface MessageListener {
        void onMessage(String message);
    }

    /**
     * Utility method to extract messages from files. This method loads all
     * extracted messages into memory so if the expected number of extracted
     * messages is large, do not use this method or your application may run out
     * of memory. Use the streaming version of the method instead.
     *
     * @param file
     * @return a list of extracted messages
     * @throws IOException
     * @throws ProtocolCodecException
     * @see #extractMessages(File,
     *      quickfix.mina.message.FIXMessageDecoder.MessageListener)
     */
    public List extractMessages(File file) throws IOException, ProtocolCodecException {
        final List messages = new ArrayList();
        extractMessages(file, new MessageListener() {
            public void onMessage(String message) {
                messages.add(message);
            }
        });
        return messages;
    }

    /**
     * Utility to extract messages from a file. This method will return each
     * message found to a provided listener. The message file will also be memory
     * mapped rather than fully loaded into physical memory. Therefore, a large
     * message file can be processed without using excessive memory.
     *
     * @param file
     * @param listener
     * @throws IOException
     * @throws ProtocolCodecException
     */
    public void extractMessages(File file, final MessageListener listener) throws IOException,
            ProtocolCodecException {
        // Set up a read-only memory-mapped file
        RandomAccessFile fileIn = new RandomAccessFile(file, "r");
        FileChannel readOnlyChannel = fileIn.getChannel();
        MappedByteBuffer memoryMappedBuffer = readOnlyChannel.map(FileChannel.MapMode.READ_ONLY, 0,
                (int) readOnlyChannel.size());

        decode(null, IoBuffer.wrap(memoryMappedBuffer), new ProtocolDecoderOutput() {

            public void write(Object message) {
                listener.onMessage((String) message);
            }

            public void flush(IoFilter.NextFilter nextFilter, IoSession ioSession) {
                // ignored
            }
        });
        readOnlyChannel.close();
        fileIn.close();
    }

    private static byte[] getBytes(String s) {
        try {
            return s.getBytes(CharsetSupport.getDefaultCharset());
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy