/******************************************************************************* * Copyright (c) quickfixj.org All rights reserved. * * This file is part of the QuickFIX FIX Engine * * This file may be distributed under the terms of the quickfixj.org * license as defined by quickfixj.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 ask@quickfixj.org if any conditions of this licensing * are not clear to you. ******************************************************************************/ package quickfix; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import quickfix.Message.Header; import com.github.lburgazzoli.quickfixj.core.IFIXContext; import quickfix.field.ApplVerID; import quickfix.field.BeginSeqNo; import quickfix.field.BeginString; import quickfix.field.BusinessRejectReason; import quickfix.field.DefaultApplVerID; import quickfix.field.EncryptMethod; import quickfix.field.EndSeqNo; import quickfix.field.GapFillFlag; import quickfix.field.HeartBtInt; import quickfix.field.LastMsgSeqNumProcessed; import quickfix.field.MsgSeqNum; import quickfix.field.MsgType; import quickfix.field.NewSeqNo; import quickfix.field.NextExpectedMsgSeqNum; import quickfix.field.OrigSendingTime; import quickfix.field.PossDupFlag; import quickfix.field.RefMsgType; import quickfix.field.RefSeqNum; import quickfix.field.RefTagID; import quickfix.field.ResetSeqNumFlag; import quickfix.field.SenderCompID; import quickfix.field.SenderLocationID; import quickfix.field.SenderSubID; import quickfix.field.SendingTime; import quickfix.field.SessionRejectReason; import quickfix.field.SessionStatus; import quickfix.field.TargetCompID; import quickfix.field.TargetLocationID; import quickfix.field.TargetSubID; import quickfix.field.TestReqID; import quickfix.field.Text; import java.io.Closeable; import java.io.IOException; import java.net.InetAddress; import java.util.ArrayList; import java.util.Date; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import static quickfix.LogUtil.logThrowable; /** * The Session is the primary FIX abstraction for message communication. It * performs sequencing and error recovery and represents a communication channel * to a counterparty. Sessions are independent of specific communication layer * connections. A Session is defined as starting with message sequence number of 1 * and ending when the session is reset. The Session could span many sequential * connections (it cannot operate on multiple connection simultaneously). */ public class Session implements Closeable { protected final static Logger log = LoggerFactory.getLogger(Session.class); private final Application application; private final SessionID sessionID; private final SessionSchedule sessionSchedule; private final MessageFactory messageFactory; // @GuardedBy(this) private final SessionState state; private boolean enabled; private final String responderSync = new String("SessionResponderSync"); // @GuardedBy(responderSync) private Responder responder; private final IFIXContext context; // // The session time checks were causing performance problems // so we are caching the last session time check result and // only recalculating it if it's been at least 1 second since // the last check // // @GuardedBy(this) private long lastSessionTimeCheck = 0; private boolean lastSessionTimeResult = false; private int logonAttempts = 0; private long lastSessionLogon = 0; private final DataDictionaryProvider dataDictionaryProvider; private final boolean checkLatency; private final int maxLatency; private int resendRequestChunkSize = 0; private final boolean resetOnLogon; private final boolean resetOnLogout; private final boolean resetOnDisconnect; private final boolean resetOnError; private final boolean disconnectOnError; private final boolean millisecondsInTimeStamp; private final boolean refreshMessageStoreAtLogon; private final boolean redundantResentRequestsAllowed; private final boolean persistMessages; private final boolean checkCompID; private final boolean useClosedRangeForResend; private boolean disableHeartBeatCheck = false; private boolean rejectInvalidMessage = false; private boolean rejectMessageOnUnhandledException = false; private boolean requiresOrigSendingTime = false; private boolean forceResendWhenCorruptedStore = false; private boolean enableNextExpectedMsgSeqNum = false; private boolean enableLastMsgSeqNumProcessed = false; private final AtomicBoolean isResetting = new AtomicBoolean(); private final ListenerSupport stateListeners = new ListenerSupport(SessionStateListener.class); private final SessionStateListener stateListener = (SessionStateListener) stateListeners .getMulticaster(); private final AtomicReference<ApplVerID> targetDefaultApplVerID = new AtomicReference<ApplVerID>(); private final DefaultApplVerID senderDefaultApplVerID; private boolean validateSequenceNumbers = true; private boolean validateIncomingMessage = true; private final int[] logonIntervals; private final Set<InetAddress> allowedRemoteAddresses; public static final int DEFAULT_MAX_LATENCY = 120; public static final int DEFAULT_RESEND_RANGE_CHUNK_SIZE = 0; //no resend range public static final double DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER = 0.5; Session(IFIXContext context,Application application, MessageStoreFactory messageStoreFactory, SessionID sessionID, DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule, LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval) { this(context,application, messageStoreFactory, sessionID, dataDictionaryProvider, sessionSchedule, logFactory, messageFactory, heartbeatInterval, true, DEFAULT_MAX_LATENCY, true, false, false, false, false, true, false, true, false, DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, null, true, new int[] { 5 }, false, false, false, true, false, true, false, null, true, DEFAULT_RESEND_RANGE_CHUNK_SIZE, false, false); } Session(IFIXContext context,Application application, MessageStoreFactory messageStoreFactory, SessionID sessionID, DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule, LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval, boolean checkLatency, int maxLatency, boolean millisecondsInTimeStamp, boolean resetOnLogon, boolean resetOnLogout, boolean resetOnDisconnect, boolean refreshMessageStoreAtLogon, boolean checkCompID, boolean redundantResentRequestsAllowed, boolean persistMessages, boolean useClosedRangeForResend, double testRequestDelayMultiplier, DefaultApplVerID senderDefaultApplVerID, boolean validateSequenceNumbers, int[] logonIntervals, boolean resetOnError, boolean disconnectOnError, boolean disableHeartBeatCheck, boolean rejectInvalidMessage, boolean rejectMessageOnUnhandledException, boolean requiresOrigSendingTime, boolean forceResendWhenCorruptedStore, Set<InetAddress> allowedRemoteAddresses, boolean validateIncomingMessage, int resendRequestChunkSize, boolean enableNextExpectedMsgSeqNum, boolean enableLastMsgSeqNumProcessed) { this.context = context; this.application = application; this.sessionID = sessionID; this.sessionSchedule = sessionSchedule; this.checkLatency = checkLatency; this.maxLatency = maxLatency; this.resetOnLogon = resetOnLogon; this.resetOnLogout = resetOnLogout; this.resetOnDisconnect = resetOnDisconnect; this.millisecondsInTimeStamp = millisecondsInTimeStamp; this.refreshMessageStoreAtLogon = refreshMessageStoreAtLogon; this.dataDictionaryProvider = dataDictionaryProvider; this.messageFactory = messageFactory; this.checkCompID = checkCompID; this.redundantResentRequestsAllowed = redundantResentRequestsAllowed; this.persistMessages = persistMessages; this.useClosedRangeForResend = useClosedRangeForResend; this.senderDefaultApplVerID = senderDefaultApplVerID; this.logonIntervals = logonIntervals; this.resetOnError = resetOnError; this.disconnectOnError = disconnectOnError; this.disableHeartBeatCheck = disableHeartBeatCheck; this.rejectInvalidMessage = rejectInvalidMessage; this.rejectMessageOnUnhandledException = rejectMessageOnUnhandledException; this.requiresOrigSendingTime = requiresOrigSendingTime; this.forceResendWhenCorruptedStore = forceResendWhenCorruptedStore; this.allowedRemoteAddresses = allowedRemoteAddresses; this.validateIncomingMessage = validateIncomingMessage; this.validateSequenceNumbers = validateSequenceNumbers; this.resendRequestChunkSize = resendRequestChunkSize; this.enableNextExpectedMsgSeqNum = enableNextExpectedMsgSeqNum; this.enableLastMsgSeqNumProcessed = enableLastMsgSeqNumProcessed; final Log engineLog = (logFactory != null) ? logFactory.create(sessionID) : null; if (engineLog instanceof SessionStateListener) { addStateListener((SessionStateListener) engineLog); } final MessageStore messageStore = messageStoreFactory.create(sessionID); if (messageStore instanceof SessionStateListener) { addStateListener((SessionStateListener) messageStore); } state = new SessionState(this, engineLog, heartbeatInterval, heartbeatInterval != 0, messageStore, testRequestDelayMultiplier); context.addSession(this); getLog().onEvent("Session " + sessionID + " schedule is " + sessionSchedule); try { if (!checkSessionTime()) { getLog().onEvent("Session state is not current; resetting " + sessionID); reset(); } } catch (final IOException e) { LogUtil.logThrowable(getLog(), "error during session construction", e); } //QFJ-721: for non-FIXT sessions we do not need to set targetDefaultApplVerID from Logon if (!sessionID.isFIXT()) { targetDefaultApplVerID.set(MessageUtils.toApplVerID(sessionID.getBeginString())); } setEnabled(true); getLog().onEvent("Created session: " + sessionID); } public MessageFactory getMessageFactory() { return messageFactory; } /** * Registers a responder with the session. This is used by the acceptor and * initiator implementations. * * @param responder a responder implementation */ public void setResponder(Responder responder) { synchronized (responderSync) { this.responder = responder; if (responder != null) { stateListener.onConnect(); } else { stateListener.onDisconnect(); } } } public Responder getResponder() { synchronized (responderSync) { return responder; } } public IFIXContext getContext() { return context; } /** * This should not be used by end users. * * @return the Session's connection responder */ public boolean hasResponder() { return getResponder() != null; } private synchronized boolean checkSessionTime() throws IOException { if (sessionSchedule == null) { return true; } // Only check the session time once per second at most. It isn't // necessary to do for every message received. // QFJ-357/QFJ-716/QFJ-527: Exception: We will check the session time again // if lastSessionTimeResult was false. This is because lastSessionTimeResult // will always be false when this method is called from the constructor and // hence a session that is started within one second after creation will get // disconnected immediately. final Date date = SystemTime.getDate(); if (!lastSessionTimeResult || (date.getTime() - lastSessionTimeCheck) >= 1000L) { final Date getSessionCreationTime = state.getCreationTime(); lastSessionTimeResult = sessionSchedule.isSameSession(SystemTime.getUtcCalendar(date), SystemTime.getUtcCalendar(getSessionCreationTime)); lastSessionTimeCheck = date.getTime(); return lastSessionTimeResult; } else { return lastSessionTimeResult; } } /** * This method can be used to manually logon to a FIX session. */ public void logon() { state.clearLogoutReason(); setEnabled(true); } private synchronized void setEnabled(boolean enabled) { this.enabled = enabled; } private void initializeHeader(Message.Header header) { state.setLastSentTime(SystemTime.currentTimeMillis()); header.setString(BeginString.FIELD, sessionID.getBeginString()); header.setString(SenderCompID.FIELD, sessionID.getSenderCompID()); optionallySetID(header, SenderSubID.FIELD, sessionID.getSenderSubID()); optionallySetID(header, SenderLocationID.FIELD, sessionID.getSenderLocationID()); header.setString(TargetCompID.FIELD, sessionID.getTargetCompID()); optionallySetID(header, TargetSubID.FIELD, sessionID.getTargetSubID()); optionallySetID(header, TargetLocationID.FIELD, sessionID.getTargetLocationID()); header.setInt(MsgSeqNum.FIELD, getExpectedSenderNum()); insertSendingTime(header); } private void optionallySetID(Header header, int field, String value) { if (!value.equals(SessionID.NOT_SET)) { header.setString(field, value); } } private void insertSendingTime(Message.Header header) { final boolean includeMillis = sessionID.getBeginString().compareTo( FixVersions.BEGINSTRING_FIX42) >= 0 && millisecondsInTimeStamp; header.setUtcTimeStamp(SendingTime.FIELD, SystemTime.getDate(), includeMillis); } /** * This method can be used to manually logout of a FIX session. */ public void logout() { setEnabled(false); } /** * This method can be used to manually logout of a FIX session. * @param reason this will be included in the logout message */ public void logout(String reason) { state.setLogoutReason(reason); logout(); } /** * Used internally * * @return true if session is enabled, false otherwise. */ public synchronized boolean isEnabled() { return enabled; } /** * Predicate indicating whether a logon message has been sent. * * (QF Compatibility) * * @return true if logon message was sent, false otherwise. */ public boolean sentLogon() { return state.isLogonSent(); } /** * Predicate indicating whether a logon message has been received. * * (QF Compatibility) * * @return true if logon message was received, false otherwise. */ public boolean receivedLogon() { return state.isLogonReceived(); } /** * Predicate indicating whether a logout message has been sent. * * (QF Compatibility) * * @return true if logout message was sent, false otherwise. */ public boolean sentLogout() { return state.isLogoutSent(); } /** * Predicate indicating whether a logout message has been received. This can * be used to determine if a session ended with an unexpected disconnect. * * @return true if logout message has been received, false otherwise. */ public boolean receivedLogout() { return state.isLogoutReceived(); } /** * Is the session logged on. * * @return true if logged on, false otherwise. */ public boolean isLoggedOn() { return sentLogon() && receivedLogon(); } private boolean isResetNeeded() { return sessionID.getBeginString().compareTo(FixVersions.BEGINSTRING_FIX41) >= 0 && (resetOnLogon || resetOnLogout || resetOnDisconnect) && getExpectedSenderNum() == 1 && getExpectedTargetNum() == 1; } /** * Logs out and disconnects session (if logged on) and then resets session state. * * @throws IOException IO error * @see SessionState#reset() */ public void reset() throws IOException { if (!isResetting.compareAndSet(false, true)) { return; } try { if (hasResponder() && isLoggedOn()) { if (application instanceof ApplicationExtended) { ((ApplicationExtended) application).onBeforeSessionReset(sessionID); } generateLogout(); disconnect("Session reset", false); } resetState(); } finally { isResetting.set(false); } } /** * Set the next outgoing message sequence number. This method is not * synchronized. * * @param num next outgoing sequence number * @throws IOException IO error */ public void setNextSenderMsgSeqNum(int num) throws IOException { state.getMessageStore().setNextSenderMsgSeqNum(num); } /** * Set the next expected target message sequence number. This method is not * synchronized. * * @param num next expected target sequence number * @throws IOException IO error */ public void setNextTargetMsgSeqNum(int num) throws IOException { state.getMessageStore().setNextTargetMsgSeqNum(num); } /** * Retrieves the expected sender sequence number. This method is not * synchronized. * * @return next expected sender sequence number */ public int getExpectedSenderNum() { try { return state.getMessageStore().getNextSenderMsgSeqNum(); } catch (final IOException e) { getLog().onErrorEvent("getNextSenderMsgSeqNum failed: " + e.getMessage()); return -1; } } /** * Retrieves the expected target sequence number. This method is not * synchronized. * * @return next expected target sequence number */ public int getExpectedTargetNum() { try { return state.getMessageStore().getNextTargetMsgSeqNum(); } catch (final IOException e) { getLog().onErrorEvent("getNextTargetMsgSeqNum failed: " + e.getMessage()); return -1; } } public Log getLog() { return state.getLog(); } /** * Get the message store. (QF Compatibility) * @return the message store */ public MessageStore getStore() { return state.getMessageStore(); } /** * (Internal use only) */ public void next(Message message) throws FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException, InvalidMessage { final Header header = message.getHeader(); final String msgType = header.getString(MsgType.FIELD); // QFJ-650 if (!header.isSetField(MsgSeqNum.FIELD)) { generateLogout("Received message without MsgSeqNum"); disconnect("Received message without MsgSeqNum: " + message, true); return; } final String sessionBeginString = sessionID.getBeginString(); try { final String beginString = header.getString(BeginString.FIELD); if (!beginString.equals(sessionBeginString)) { throw new UnsupportedVersion("Message version '" + beginString + "' does not match the session version '" + sessionBeginString + "'"); } if (msgType.equals(MsgType.LOGON)) { if (sessionID.isFIXT()) { targetDefaultApplVerID.set(new ApplVerID(message .getString(DefaultApplVerID.FIELD))); } // QFJ-648 if (message.isSetField(HeartBtInt.FIELD)) { if (message.getInt(HeartBtInt.FIELD) < 0) { throw new RejectLogon("HeartBtInt must not be negative"); } } } if (validateIncomingMessage && dataDictionaryProvider != null) { final DataDictionary sessionDataDictionary = dataDictionaryProvider .getSessionDataDictionary(beginString); final ApplVerID applVerID = header.isSetField(ApplVerID.FIELD) ? new ApplVerID( header.getString(ApplVerID.FIELD)) : targetDefaultApplVerID.get(); final DataDictionary applicationDataDictionary = MessageUtils .isAdminMessage(msgType) ? dataDictionaryProvider .getSessionDataDictionary(beginString) : dataDictionaryProvider .getApplicationDataDictionary(applVerID); // related to QFJ-367 : just warn invalid incoming field/tags try { DataDictionary.validate(message, sessionDataDictionary, applicationDataDictionary); } catch (final IncorrectTagValue e) { if (rejectInvalidMessage) { throw e; } else { getLog().onErrorEvent("Warn: incoming message with " + e + ": " + message); } } catch (final FieldException e) { if (message.isSetField(e.getField())) { if (rejectInvalidMessage) { throw e; } else { getLog().onErrorEvent( "Warn: incoming message with incorrect field: " + message.getField(e.getField()) + ": " + message); } } else { if (rejectInvalidMessage) { throw e; } else { getLog().onErrorEvent( "Warn: incoming message with missing field: " + e.getField() + ": " + e.getMessage() + ": " + message); } } } catch (final FieldNotFound e) { if (rejectInvalidMessage) { throw e; } else { getLog().onErrorEvent("Warn: incoming " + e + ": " + message); } } } if (msgType.equals(MsgType.LOGON)) { nextLogon(message); } else if (msgType.equals(MsgType.HEARTBEAT)) { nextHeartBeat(message); } else if (msgType.equals(MsgType.TEST_REQUEST)) { nextTestRequest(message); } else if (msgType.equals(MsgType.SEQUENCE_RESET)) { nextSequenceReset(message); } else if (msgType.equals(MsgType.LOGOUT)) { nextLogout(message); } else if (msgType.equals(MsgType.RESEND_REQUEST)) { nextResendRequest(message); } else if (msgType.equals(MsgType.REJECT)) { nextReject(message); } else { if (!verify(message)) { return; } state.incrNextTargetMsgSeqNum(); } } catch (final FieldException e) { getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); if (resetOrDisconnectIfRequired(message)) { return; } generateReject(message, e.getSessionRejectReason(), e.getField()); } catch (final FieldNotFound e) { getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); if (resetOrDisconnectIfRequired(message)) { return; } if (sessionBeginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0 && message.isApp()) { generateBusinessReject(message, BusinessRejectReason.CONDITIONALLY_REQUIRED_FIELD_MISSING, e.field); } else { if (msgType.equals(MsgType.LOGON)) { getLog().onErrorEvent("Required field missing from logon"); disconnect("Required field missing from logon", true); } else { generateReject(message, SessionRejectReason.REQUIRED_TAG_MISSING, e.field); } } } catch (final IncorrectDataFormat e) { getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); if (resetOrDisconnectIfRequired(message)) { return; } generateReject(message, SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE, e.field); } catch (final IncorrectTagValue e) { getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); generateReject(message, SessionRejectReason.VALUE_IS_INCORRECT, e.field); } catch (final InvalidMessage e) { getLog().onErrorEvent("Skipping invalid message: " + e + ": " + message); if (resetOrDisconnectIfRequired(message)) { return; } } catch (final RejectLogon e) { final String rejectMessage = e.getMessage() != null ? (": " + e) : ""; getLog().onErrorEvent("Logon rejected" + rejectMessage); if (e.isLogoutBeforeDisconnect()) { if (e.getSessionStatus() > -1) { generateLogout(e.getMessage(), new SessionStatus(e.getSessionStatus())); } else { generateLogout(e.getMessage()); } } state.incrNextTargetMsgSeqNum(); disconnect("Logon rejected: " + e, true); } catch (final UnsupportedMessageType e) { getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); if (resetOrDisconnectIfRequired(message)) { return; } if (sessionBeginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0) { generateBusinessReject(message, BusinessRejectReason.UNSUPPORTED_MESSAGE_TYPE, 0); } else { generateReject(message, "Unsupported message type"); } } catch (final UnsupportedVersion e) { getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); if (resetOrDisconnectIfRequired(message)) { return; } if (msgType.equals(MsgType.LOGOUT)) { nextLogout(message); } else { generateLogout("Incorrect BeginString: " + e.getMessage()); state.incrNextTargetMsgSeqNum(); // 1d_InvalidLogonWrongBeginString.def appears to require // a disconnect although the C++ didn't appear to be doing it. // ??? disconnect("Incorrect BeginString: " + e, true); } } catch (final IOException e) { LogUtil.logThrowable(this, "Error processing message: " + message, e); if (resetOrDisconnectIfRequired(message)) { return; } } catch (Throwable t) { // QFJ-572 // If there are any other Throwables we might catch them here if desired. // They were most probably thrown out of fromCallback(). if (rejectMessageOnUnhandledException) { getLog().onErrorEvent("Rejecting message: " + t + ": " + message); if (resetOrDisconnectIfRequired(message)) { return; } if (!(MessageUtils.isAdminMessage(msgType)) && (sessionBeginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0)) { generateBusinessReject(message, BusinessRejectReason.APPLICATION_NOT_AVAILABLE, 0); } else { if (msgType.equals(MsgType.LOGON)) { disconnect("Problem processing Logon message", true); } else { generateReject(message, SessionRejectReason.OTHER, 0); } } } else { // Re-throw as quickfix.RuntimeError to keep close to the former behaviour // and to have a clear notion of what is thrown out of this method. // Throwing RuntimeError here means that the target seqnum is not incremented // and a resend will be triggered by the next incoming message. throw new RuntimeError(t); } } nextQueued(); if (isLoggedOn()) { next(); } } private boolean resetOrDisconnectIfRequired(Message msg) { if (!resetOnError && !disconnectOnError) { return false; } if (!isLoggedOn()) { return false; } // do not interfere in admin and logon/logout messages etc. if (msg != null && msg.isAdmin()) { return false; } if (resetOnError) { try { getLog().onErrorEvent("Auto reset"); reset(); } catch (final IOException e) { log.error("Failed reseting: " + e); } return true; } if (disconnectOnError) { try { disconnect("Auto disconnect", false); } catch (final IOException e) { log.error("Failed disconnecting: " + e); } return true; } return false; } private boolean isStateRefreshNeeded(String msgType) { return refreshMessageStoreAtLogon && !state.isInitiator() && msgType.equals(MsgType.LOGON); } private void nextReject(Message reject) throws FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException, InvalidMessage { if (!verify(reject, false, validateSequenceNumbers)) { return; } state.incrNextTargetMsgSeqNum(); nextQueued(); } private void nextResendRequest(Message resendRequest) throws IOException, RejectLogon, FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, InvalidMessage { if (!verify(resendRequest, false, false)) { return; } final int beginSeqNo = resendRequest.getInt(BeginSeqNo.FIELD); final int endSeqNo = resendRequest.getInt(EndSeqNo.FIELD); getLog().onEvent( "Received ResendRequest FROM: " + beginSeqNo + " TO: " + formatEndSeqNum(endSeqNo)); manageGapFill(resendRequest, beginSeqNo, endSeqNo); } /** * A Gap has been request to be filled by either a resend request or on a logon message * @param messageOutSync the message that caused the gap to be filled * @param beginSeqNo the seqNum of the first missing message * @param endSeqNo the seqNum of the last missing message * @throws FieldNotFound * @throws IOException * @throws InvalidMessage */ private void manageGapFill(Message messageOutSync, int beginSeqNo, int endSeqNo) throws FieldNotFound, IOException, InvalidMessage { // Adjust the ending sequence number for older versions of FIX final String beginString = sessionID.getBeginString(); final int expectedSenderNum = getExpectedSenderNum(); if (beginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0 && endSeqNo == 0 || beginString.compareTo(FixVersions.BEGINSTRING_FIX42) <= 0 && endSeqNo == 999999 || endSeqNo >= expectedSenderNum) { endSeqNo = expectedSenderNum - 1; } // Just do a gap fill when messages aren't persisted if (!persistMessages) { endSeqNo += 1; final int next = state.getNextSenderMsgSeqNum(); if (endSeqNo > next) { endSeqNo = next; } generateSequenceReset(messageOutSync, beginSeqNo, endSeqNo); } else { resendMessages(messageOutSync, beginSeqNo, endSeqNo); } final int resendRequestMsgSeqNum = messageOutSync.getHeader().getInt(MsgSeqNum.FIELD); if (!isTargetTooHigh(resendRequestMsgSeqNum) && !isTargetTooLow(resendRequestMsgSeqNum)) { state.incrNextTargetMsgSeqNum(); } } private String formatEndSeqNum(int seqNo) { return (seqNo == 0 ? "infinity" : Integer.toString(seqNo)); } private Message parseMessage(String messageData) throws InvalidMessage { return MessageUtils.parse(this, messageData); } private boolean isTargetTooLow(int msgSeqNum) throws IOException { return msgSeqNum < state.getNextTargetMsgSeqNum(); } /** * * @param receivedMessage if not null, it is the message received and upon which the resend request is generated * @param beginSeqNo * @param endSeqNo * @throws FieldNotFound */ private void generateSequenceReset(Message receivedMessage, int beginSeqNo, int endSeqNo) throws FieldNotFound { final Message sequenceReset = messageFactory.create(sessionID.getBeginString(), MsgType.SEQUENCE_RESET); final int newSeqNo = endSeqNo; final Header header = sequenceReset.getHeader(); header.setBoolean(PossDupFlag.FIELD, true); initializeHeader(header); header.setUtcTimeStamp(OrigSendingTime.FIELD, header.getUtcTimeStamp(SendingTime.FIELD)); header.setInt(MsgSeqNum.FIELD, beginSeqNo); sequenceReset.setInt(NewSeqNo.FIELD, newSeqNo); sequenceReset.setBoolean(GapFillFlag.FIELD, true); if (receivedMessage != null && enableLastMsgSeqNumProcessed) { try { sequenceReset.getHeader().setInt(LastMsgSeqNumProcessed.FIELD, receivedMessage.getHeader().getInt(MsgSeqNum.FIELD)); } catch (final FieldNotFound e) { //should not happen as MsgSeqNum must be present getLog().onErrorEvent("Received message without MsgSeqNum " + receivedMessage); } } sendRaw(sequenceReset, beginSeqNo); getLog().onEvent("Sent SequenceReset TO: " + newSeqNo); } private boolean resendApproved(Message message) throws FieldNotFound { try { application.toApp(context,message, sessionID); } catch (final DoNotSend e) { return false; } catch (final Throwable t) { // Any exception other than DoNotSend will not stop the message from being resent logApplicationException("toApp() during resend", t); } return true; } private void initializeResendFields(Message message) throws FieldNotFound { final Message.Header header = message.getHeader(); final Date sendingTime = header.getUtcTimeStamp(SendingTime.FIELD); header.setUtcTimeStamp(OrigSendingTime.FIELD, sendingTime); header.setBoolean(PossDupFlag.FIELD, true); insertSendingTime(header); } private void logApplicationException(String location, Throwable t) { logThrowable(getLog(), "Application exception in " + location, t); } private void nextLogout(Message logout) throws IOException, RejectLogon, FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { if (!verify(logout, false, false)) { return; } String msg; if (!state.isLogoutSent()) { msg = "Received logout request"; if (logout.isSetField(Text.FIELD)) { msg += ": " + logout.getString(Text.FIELD); } getLog().onEvent(msg); generateLogout(logout); getLog().onEvent("Sent logout response"); } else { msg = "Received logout response"; getLog().onEvent(msg); } state.setLogoutReceived(true); state.incrNextTargetMsgSeqNum(); if (resetOnLogout) { resetState(); } disconnect(msg, false); } public void generateLogout() { generateLogout(null, null, null); } private void generateLogout(Message otherLogout) { generateLogout(otherLogout, null, null); } private void generateLogout(String reason) { generateLogout(null, reason, null); } private void generateLogout(String reason, SessionStatus sessionStatus) { generateLogout(null, reason, sessionStatus); } /** * To generate a logout message * @param otherLogout if not null, the logout message that is causing a logout to be sent * @param text */ private void generateLogout(Message otherLogout, String text, SessionStatus sessionStatus) { final Message logout = messageFactory.create(sessionID.getBeginString(), MsgType.LOGOUT); initializeHeader(logout.getHeader()); if (text != null && !"".equals(text)) { logout.setString(Text.FIELD, text); } if (sessionStatus != null) { logout.setInt(SessionStatus.FIELD, sessionStatus.getValue()); } if (otherLogout != null && enableLastMsgSeqNumProcessed) { try { logout.getHeader().setInt(LastMsgSeqNumProcessed.FIELD, otherLogout.getHeader().getInt(MsgSeqNum.FIELD)); } catch (final FieldNotFound e) { //should not happen as MsgSeqNum must be present getLog().onErrorEvent("Received logout without MsgSeqNum"); } } sendRaw(logout, 0); state.setLogoutSent(true); } private void nextSequenceReset(Message sequenceReset) throws IOException, RejectLogon, FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { boolean isGapFill = false; if (sequenceReset.isSetField(GapFillFlag.FIELD)) { isGapFill = sequenceReset.getBoolean(GapFillFlag.FIELD) && validateSequenceNumbers; } if (!verify(sequenceReset, isGapFill, isGapFill)) { return; } if (validateSequenceNumbers && sequenceReset.isSetField(NewSeqNo.FIELD)) { final int newSequence = sequenceReset.getInt(NewSeqNo.FIELD); getLog().onEvent( "Received SequenceReset FROM: " + getExpectedTargetNum() + " TO: " + newSequence); if (newSequence > getExpectedTargetNum()) { final int[] range = state.getResendRange(); if (range[2] > 0) { if (newSequence >= range[1]) { state.setNextTargetMsgSeqNum(newSequence); } else if (newSequence >= range[2]) { state.setNextTargetMsgSeqNum(newSequence + 1); final String beginString = sequenceReset.getHeader().getString( BeginString.FIELD); sendResendRequest(beginString, range[1] + 1, newSequence + 1, range[1]); } } else { state.setNextTargetMsgSeqNum(newSequence); } } else if (newSequence < getExpectedTargetNum()) { getLog().onErrorEvent( "Invalid SequenceReset: newSequence=" + newSequence + " < expected=" + getExpectedTargetNum()); if (resetOrDisconnectIfRequired(sequenceReset)) { return; } generateReject(sequenceReset, SessionRejectReason.VALUE_IS_INCORRECT, NewSeqNo.FIELD); } } } private void generateReject(Message message, String str) throws FieldNotFound, IOException { final String beginString = sessionID.getBeginString(); final Message reject = messageFactory.create(beginString, MsgType.REJECT); final Header header = message.getHeader(); reject.reverseRoute(header); initializeHeader(reject.getHeader()); final String msgType = header.getString(MsgType.FIELD); final String msgSeqNum = header.getString(MsgSeqNum.FIELD); if (beginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0) { reject.setString(RefMsgType.FIELD, msgType); } reject.setString(RefSeqNum.FIELD, msgSeqNum); if (!msgType.equals(MsgType.LOGON) && !msgType.equals(MsgType.SEQUENCE_RESET) && !isPossibleDuplicate(message)) { state.incrNextTargetMsgSeqNum(); } reject.setString(Text.FIELD, str); sendRaw(reject, 0); getLog().onErrorEvent("Reject sent for Message " + msgSeqNum + ": " + str); } private boolean isPossibleDuplicate(Message message) throws FieldNotFound { final Header header = message.getHeader(); return header.isSetField(PossDupFlag.FIELD) && header.getBoolean(PossDupFlag.FIELD); } private void generateReject(Message message, int err, int field) throws IOException, FieldNotFound { final String reason = SessionRejectReasonText.getMessage(err); if (!state.isLogonReceived()) { final String errorMessage = "Tried to send a reject while not logged on: " + reason + " (field " + field + ")"; throw new SessionException(errorMessage); } final String beginString = sessionID.getBeginString(); final Message reject = messageFactory.create(beginString, MsgType.REJECT); final Header header = message.getHeader(); reject.reverseRoute(header); initializeHeader(reject.getHeader()); String msgType = ""; if (header.isSetField(MsgType.FIELD)) { msgType = header.getString(MsgType.FIELD); } int msgSeqNum = 0; if (header.isSetField(MsgSeqNum.FIELD)) { msgSeqNum = header.getInt(MsgSeqNum.FIELD); reject.setInt(RefSeqNum.FIELD, msgSeqNum); } if (beginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0) { if (!msgType.equals("")) { reject.setString(RefMsgType.FIELD, msgType); } if (beginString.compareTo(FixVersions.BEGINSTRING_FIX44) > 0) { reject.setInt(SessionRejectReason.FIELD, err); } else if (beginString.compareTo(FixVersions.BEGINSTRING_FIX44) == 0) { if (err == SessionRejectReason.OTHER || err <= SessionRejectReason.NON_DATA_VALUE_INCLUDES_FIELD_DELIMITER) { reject.setInt(SessionRejectReason.FIELD, err); } } else if (beginString.compareTo(FixVersions.BEGINSTRING_FIX43) == 0) { if (err <= SessionRejectReason.NON_DATA_VALUE_INCLUDES_FIELD_DELIMITER) { reject.setInt(SessionRejectReason.FIELD, err); } } else if (beginString.compareTo(FixVersions.BEGINSTRING_FIX42) == 0) { if (err <= SessionRejectReason.INVALID_MSGTYPE) { reject.setInt(SessionRejectReason.FIELD, err); } } } // This is a set and increment of target msg sequence number, the sequence // number must be locked to guard against race conditions. state.lockTargetMsgSeqNum(); try { if (!msgType.equals(MsgType.LOGON) && !msgType.equals(MsgType.SEQUENCE_RESET) && (msgSeqNum == getExpectedTargetNum() || !isPossibleDuplicate(message))) { state.incrNextTargetMsgSeqNum(); } } finally { state.unlockTargetMsgSeqNum(); } if (reason != null && (field > 0 || err == SessionRejectReason.INVALID_TAG_NUMBER)) { setRejectReason(reject, field, reason, true); getLog().onErrorEvent( "Reject sent for Message " + msgSeqNum + ": " + reason + ":" + field); } else if (reason != null) { setRejectReason(reject, reason); getLog().onErrorEvent("Reject sent for Message " + msgSeqNum + ": " + reason); } else { getLog().onErrorEvent("Reject sent for Message " + msgSeqNum); } if (enableLastMsgSeqNumProcessed) { reject.getHeader().setInt(LastMsgSeqNumProcessed.FIELD, message.getHeader().getInt(MsgSeqNum.FIELD)); } sendRaw(reject, 0); } private void setRejectReason(Message reject, String reason) { reject.setString(Text.FIELD, reason); } private void setRejectReason(Message reject, int field, String reason, boolean includeFieldInReason) { boolean isRejectMessage; try { isRejectMessage = MsgType.REJECT.equals(reject.getHeader().getString(MsgType.FIELD)); } catch (final FieldNotFound e) { isRejectMessage = false; } if (isRejectMessage && sessionID.getBeginString().compareTo(FixVersions.BEGINSTRING_FIX42) >= 0) { reject.setInt(RefTagID.FIELD, field); reject.setString(Text.FIELD, reason); } else { reject.setString(Text.FIELD, reason + (includeFieldInReason ? " (" + field + ")" : "")); } } private void generateBusinessReject(Message message, int err, int field) throws FieldNotFound, IOException { final Message reject = messageFactory.create(sessionID.getBeginString(), MsgType.BUSINESS_MESSAGE_REJECT); initializeHeader(reject.getHeader()); final String msgType = message.getHeader().getString(MsgType.FIELD); final String msgSeqNum = message.getHeader().getString(MsgSeqNum.FIELD); reject.setString(RefMsgType.FIELD, msgType); reject.setString(RefSeqNum.FIELD, msgSeqNum); reject.setInt(BusinessRejectReason.FIELD, err); state.incrNextTargetMsgSeqNum(); final String reason = BusinessRejectReasonText.getMessage(err); setRejectReason(reject, field, reason, field != 0); getLog().onErrorEvent( "Reject sent for Message " + msgSeqNum + (reason != null ? (": " + reason) : "") + (field != 0 ? (": tag=" + field) : "")); sendRaw(reject, 0); } private void nextTestRequest(Message testRequest) throws FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException, InvalidMessage { if (!verify(testRequest)) { return; } generateHeartbeat(testRequest); state.incrNextTargetMsgSeqNum(); nextQueued(); } private void generateHeartbeat(Message testRequest) throws FieldNotFound { final Message heartbeat = messageFactory.create(sessionID.getBeginString(), MsgType.HEARTBEAT); initializeHeader(heartbeat.getHeader()); if (testRequest.isSetField(TestReqID.FIELD)) { heartbeat.setString(TestReqID.FIELD, testRequest.getString(TestReqID.FIELD)); } if (enableLastMsgSeqNumProcessed) { heartbeat.getHeader().setInt(LastMsgSeqNumProcessed.FIELD, testRequest.getHeader().getInt(MsgSeqNum.FIELD)); } sendRaw(heartbeat, 0); } private void nextHeartBeat(Message heartBeat) throws FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException, InvalidMessage { if (!verify(heartBeat)) { return; } state.incrNextTargetMsgSeqNum(); nextQueued(); } private boolean verify(Message msg, boolean checkTooHigh, boolean checkTooLow) throws RejectLogon, FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException { state.setLastReceivedTime(SystemTime.currentTimeMillis()); state.clearTestRequestCounter(); String msgType; try { final Message.Header header = msg.getHeader(); final String senderCompID = header.getString(SenderCompID.FIELD); final String targetCompID = header.getString(TargetCompID.FIELD); final Date sendingTime = header.getUtcTimeStamp(SendingTime.FIELD); msgType = header.getString(MsgType.FIELD); int msgSeqNum = 0; if (checkTooHigh || checkTooLow) { msgSeqNum = header.getInt(MsgSeqNum.FIELD); } if (!validLogonState(msgType)) { throw new SessionException("Logon state is not valid for message (MsgType=" + msgType + ")"); } if (!isGoodTime(sendingTime)) { doBadTime(msg); return false; } if (!isCorrectCompID(senderCompID, targetCompID)) { doBadCompID(msg); return false; } if (checkTooHigh && isTargetTooHigh(msgSeqNum)) { doTargetTooHigh(msg); return false; } else if (checkTooLow && isTargetTooLow(msgSeqNum)) { doTargetTooLow(msg); return false; } // Handle poss dup where msgSeq is as expected // FIX 4.4 Vol 2, examples case 2f&g if (isPossibleDuplicate(msg) && !validatePossDup(msg)) { return false; } if ((checkTooHigh || checkTooLow) && state.isResendRequested()) { final int[] range; synchronized (state.getLock()) { range = state.getResendRange(); if (msgSeqNum >= range[1]) { getLog().onEvent( "ResendRequest for messages FROM " + range[0] + " TO " + range[1] + " has been satisfied."); state.setResendRange(0, 0, 0); } } if (msgSeqNum < range[1] && range[2] > 0 && msgSeqNum >= range[2]) { final String beginString = header.getString(BeginString.FIELD); sendResendRequest(beginString, range[1] + 1, msgSeqNum + 1, range[1]); } } } catch (final FieldNotFound e) { throw e; } catch (final Exception e) { getLog().onErrorEvent(e.getClass().getName() + " " + e.getMessage()); disconnect("Verifying message failed: " + e, true); return false; } fromCallback(msgType, msg, sessionID); return true; } private boolean doTargetTooLow(Message msg) throws FieldNotFound, IOException { if (!isPossibleDuplicate(msg)) { final int msgSeqNum = msg.getHeader().getInt(MsgSeqNum.FIELD); final String text = "MsgSeqNum too low, expecting " + getExpectedTargetNum() + " but received " + msgSeqNum; generateLogout(text); throw new SessionException(text); } return validatePossDup(msg); } private void doBadCompID(Message msg) throws IOException, FieldNotFound { generateReject(msg, SessionRejectReason.COMPID_PROBLEM, 0); generateLogout(); } private void doBadTime(Message msg) throws IOException, FieldNotFound { try { generateReject(msg, SessionRejectReason.SENDINGTIME_ACCURACY_PROBLEM, 0); generateLogout(); } catch (final SessionException ex) { generateLogout(ex.getMessage()); throw ex; } } private boolean isGoodTime(Date sendingTime) { if (!checkLatency) { return true; } return Math.abs(SystemTime.currentTimeMillis() - sendingTime.getTime()) / 1000 <= maxLatency; } private void fromCallback(String msgType, Message msg, SessionID sessionID2) throws RejectLogon, FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { // Application exceptions will prevent the incoming sequence number from being incremented // and may result in resend requests and the next startup. This way, a buggy application // can be fixed and then reprocess previously sent messages. // QFJ-572: Behaviour depends on the setting of flag rejectMessageOnUnhandledException. if (MessageUtils.isAdminMessage(msgType)) { application.fromAdmin(context,msg, sessionID); } else { application.fromApp(context,msg, sessionID); } } private synchronized boolean validLogonState(String msgType) { if (msgType.equals(MsgType.LOGON) && state.isResetSent() || state.isResetReceived()) { return true; } if (msgType.equals(MsgType.LOGON) && !state.isLogonReceived() || !msgType.equals(MsgType.LOGON) && state.isLogonReceived()) { return true; } if (msgType.equals(MsgType.LOGOUT) && state.isLogonSent()) { return true; } if (!msgType.equals(MsgType.LOGOUT) && state.isLogoutSent()) { return true; } if (msgType.equals(MsgType.SEQUENCE_RESET)) { return true; } if (msgType.equals(MsgType.REJECT)) { return true; } return false; } private boolean verify(Message message) throws RejectLogon, FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException { return verify(message, validateSequenceNumbers, validateSequenceNumbers); } /** * Called from the timer-related code in the acceptor/initiator * implementations. This is not typically called from application code. * * @throws IOException IO error */ public void next() throws IOException { if (!isEnabled()) { if (isLoggedOn()) { if (!state.isLogoutSent()) { getLog().onEvent("Initiated logout request"); generateLogout(state.getLogoutReason()); } } else { return; } } if (!checkSessionTime()) { reset(); return; } // Return if we are not connected if (!hasResponder()) { return; } if (!state.isLogonReceived()) { if (state.isLogonSendNeeded()) { if (isTimeToGenerateLogon()) { // ApplicationExtended can prevent the automatic login if (application instanceof ApplicationExtended) { if (!((ApplicationExtended) application).canLogon(sessionID)) { return; } } if (generateLogon()) { getLog().onEvent("Initiated logon request"); } else { getLog().onErrorEvent("Error during logon request initiation"); } } } else if (state.isLogonAlreadySent() && state.isLogonTimedOut()) { disconnect("Timed out waiting for logon response", true); } return; } if (state.getHeartBeatInterval() == 0) { return; } if (state.isLogoutTimedOut()) { disconnect("Timed out waiting for logout response", true); } if (state.isTimedOut()) { if (!disableHeartBeatCheck) { disconnect("Timed out waiting for heartbeat", true); stateListener.onHeartBeatTimeout(); } else { log.warn("Heartbeat failure detected but deactivated"); } } else { if (state.isTestRequestNeeded()) { generateTestRequest("TEST"); getLog().onEvent("Sent examples request TEST"); stateListener.onMissedHeartBeat(); } else if (state.isHeartBeatNeeded()) { generateHeartbeat(); } } } private long computeNextLogonDelayMillis() { int index = logonAttempts - 1; if (index < 0) { index = 0; } long secs; if (index >= logonIntervals.length) { secs = logonIntervals[logonIntervals.length - 1]; } else { secs = logonIntervals[index]; } return secs * 1000L; } private boolean isTimeToGenerateLogon() { return SystemTime.currentTimeMillis() - lastSessionLogon >= computeNextLogonDelayMillis(); } public void generateHeartbeat() { final Message heartbeat = messageFactory.create(sessionID.getBeginString(), MsgType.HEARTBEAT); initializeHeader(heartbeat.getHeader()); sendRaw(heartbeat, 0); } public void generateTestRequest(String id) { state.incrementTestRequestCounter(); final Message testRequest = messageFactory.create(sessionID.getBeginString(), MsgType.TEST_REQUEST); initializeHeader(testRequest.getHeader()); testRequest.setString(TestReqID.FIELD, id); sendRaw(testRequest, 0); } private boolean generateLogon() throws IOException { final Message logon = messageFactory.create(sessionID.getBeginString(), MsgType.LOGON); logon.setInt(EncryptMethod.FIELD, 0); logon.setInt(HeartBtInt.FIELD, state.getHeartBeatInterval()); if (sessionID.isFIXT()) { logon.setField(DefaultApplVerID.FIELD, senderDefaultApplVerID); } if (isStateRefreshNeeded(MsgType.LOGON)) { getLog().onEvent("Refreshing message/state store at logon"); getStore().refresh(); stateListener.onRefresh(); } if (resetOnLogon) { resetState(); } if (isResetNeeded()) { logon.setBoolean(ResetSeqNumFlag.FIELD, true); } state.setLastReceivedTime(SystemTime.currentTimeMillis()); state.clearTestRequestCounter(); state.setLogonSent(true); logonAttempts++; if (enableNextExpectedMsgSeqNum) { final int nextExpectedMsgNum = getExpectedTargetNum(); logon.setInt(NextExpectedMsgSeqNum.FIELD, nextExpectedMsgNum); state.setLastExpectedLogonNextSeqNum(nextExpectedMsgNum); } return sendRaw(logon, 0); } /** * Use disconnect(reason, logError) instead. * * @deprecated */ @Deprecated public void disconnect() throws IOException { disconnect("Other reason", true); } /** * Logs out from session and closes the network connection. * * @param reason * the reason why the session is disconnected * @param logError * set to true if this disconnection is an error * @throws IOException * IO error */ public void disconnect(String reason, boolean logError) throws IOException { try { synchronized (responderSync) { if (!hasResponder()) { getLog().onEvent("Already disconnected: " + reason); return; } final String msg = "Disconnecting: " + reason; if (logError) { getLog().onErrorEvent(msg); } else { log.info("[" + getSessionID() + "] " + msg); } responder.disconnect(); setResponder(null); } final boolean logonReceived = state.isLogonReceived(); final boolean logonSent = state.isLogonSent(); if (logonReceived || logonSent) { try { application.onLogout(context,sessionID); } catch (final Throwable t) { logApplicationException("onLogout()", t); } stateListener.onLogout(); } // QFJ-457 now enabled again if acceptor if (!state.isInitiator()) { setEnabled(true); } } finally { state.setLogonReceived(false); state.setLogonSent(false); state.setLogoutSent(false); state.setLogoutReceived(false); state.setResetReceived(false); state.setResetSent(false); state.clearQueue(); state.clearLogoutReason(); state.setResendRange(0, 0); if (resetOnDisconnect) { resetState(); } } } private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException, InvalidMessage { // QFJ-357 // If this check is not done here, the Logon would be accepted and // immediately followed by a Logout (due to check in Session.next()). if (!checkSessionTime()) { throw new RejectLogon("Logon attempt not within session time"); } if (isStateRefreshNeeded(MsgType.LOGON)) { getLog().onEvent("Refreshing message/state store at logon"); getStore().refresh(); stateListener.onRefresh(); } if (logon.isSetField(ResetSeqNumFlag.FIELD)) { state.setResetReceived(logon.getBoolean(ResetSeqNumFlag.FIELD)); } else if (state.isResetSent() && logon.getHeader().getInt(MsgSeqNum.FIELD) == 1) { // QFJ-383 getLog().onEvent( "Inferring ResetSeqNumFlag as sequence number is 1 in response to reset request"); state.setResetReceived(true); } if (state.isResetReceived()) { getLog().onEvent("Logon contains ResetSeqNumFlag=Y, resetting sequence numbers to 1"); if (!state.isResetSent()) { resetState(); } } if (state.isLogonSendNeeded() && !state.isResetReceived()) { disconnect("Received logon response before sending request", true); return; } if (!state.isInitiator() && resetOnLogon) { resetState(); } if (!verify(logon, false, validateSequenceNumbers)) { return; } //reset logout messages state.setLogoutReceived(false); state.setLogoutSent(false); state.setLogonReceived(true); // remember the expected sender sequence number of any logon response for future use final int nextSenderMsgNumAtLogonReceived = state.getMessageStore().getNextSenderMsgSeqNum(); final int sequence = logon.getHeader().getInt(MsgSeqNum.FIELD); /* * We examples here that it's not too high (which would result in a resend) and that we are not * resetting on logon 34=1 */ final boolean isLogonInNormalSequence = !(isTargetTooHigh(sequence) && !resetOnLogon); // if we have a tag 789 sent to us... if (logon.isSetField(NextExpectedMsgSeqNum.FIELD) && enableNextExpectedMsgSeqNum) { final int targetWantsNextSeqNumToBe = logon.getInt(NextExpectedMsgSeqNum.FIELD); final int actualNextNum = state.getMessageStore().getNextSenderMsgSeqNum(); // Is the 789 we received too high ?? if (targetWantsNextSeqNumToBe > actualNextNum) { // barf! we can't resend what we never sent! something unrecoverable has happened. final String err = "Tag " + NextExpectedMsgSeqNum.FIELD + " (NextExpectedMsgSeqNum) is higher than expected. Expected " + actualNextNum + ", Received " + targetWantsNextSeqNumToBe; generateLogout(err); disconnect(err, true); return; } } getLog().onEvent("Received logon"); if (!state.isInitiator()) { final int nextMsgFromTargetWeExpect = state.getMessageStore().getNextTargetMsgSeqNum(); /* * If we got one too high they need messages resent use the first message they missed (as we gap fill with that). * If we reset on logon, the current value will be 1 and we always send 2 (we haven't inc'd for current message yet +1) * If happy path (we haven't inc'd for current message yet so its current +1) */ int nextExpectedTargetNum = nextMsgFromTargetWeExpect; // we increment for the logon later (after Logon response sent) in this method if and only if in sequence if (isLogonInNormalSequence) { // logon was fine take account of it in 789 nextExpectedTargetNum++; } generateLogon(logon, nextExpectedTargetNum); } // Check for proper sequence reset response if (state.isResetSent() && !state.isResetReceived()) { disconnect("Received logon response before sending request", true); } state.setResetSent(false); state.setResetReceived(false); // Looking at the sequence number of the incoming Logon, is it too high indicating possible missed messages ? .. if (!isLogonInNormalSequence) { // if 789 was sent then we effectively have already sent a resend request if (state.isExpectedLogonNextSeqNumSent()) { // Mark state as if we have already sent a resend request from the logon's 789 (we sent) to infinity. // This will supress the resend request in doTargetTooHigh ... state.setResetRangeFromLastExpectedLogonNextSeqNumLogon(); getLog().onEvent("Required resend will be suppressed as we are setting tag 789"); } if (validateSequenceNumbers) { doTargetTooHigh(logon); } } else { state.incrNextTargetMsgSeqNum(); nextQueued(); } // Do we have a 789 if (logon.isSetField(NextExpectedMsgSeqNum.FIELD) && enableNextExpectedMsgSeqNum) { final int targetWantsNextSeqNumToBe = logon.getInt(NextExpectedMsgSeqNum.FIELD); final int actualNextNum = nextSenderMsgNumAtLogonReceived; // is the 789 lower (we checked for higher previously) than our next message after receiving the logon if (targetWantsNextSeqNumToBe != actualNextNum) { int endSeqNo = actualNextNum; // Just do a gap fill when messages aren't persisted if (!persistMessages) { endSeqNo += 1; final int next = state.getNextSenderMsgSeqNum(); if (endSeqNo > next) { endSeqNo = next; } getLog().onEvent( "Received implicit ResendRequest via Logon FROM: " + targetWantsNextSeqNumToBe + " TO: " + actualNextNum + " will be reset"); generateSequenceReset(logon, targetWantsNextSeqNumToBe, // 34= endSeqNo); // (NewSeqNo 36=) } else { // resend missed messages getLog().onEvent( "Received implicit ResendRequest via Logon FROM: " + targetWantsNextSeqNumToBe + " TO: " + actualNextNum + " will be resent"); resendMessages(logon, targetWantsNextSeqNumToBe, endSeqNo); } } } if (isLoggedOn()) { try { application.onLogon(context,sessionID); } catch (final Throwable t) { logApplicationException("onLogon()", t); } stateListener.onLogon(); lastSessionLogon = SystemTime.currentTimeMillis(); logonAttempts = 0; } } private void resendMessages(Message receivedMessage, int beginSeqNo, int endSeqNo) throws IOException, InvalidMessage, FieldNotFound { final ArrayList<String> messages = new ArrayList<String>(); try { state.get(beginSeqNo, endSeqNo, messages); } catch (final IOException e) { if (forceResendWhenCorruptedStore) { log.error("Cannot read messages from stores, resend HeartBeats", e); for (int i = beginSeqNo; i < endSeqNo; i++) { final Message heartbeat = messageFactory.create(sessionID.getBeginString(), MsgType.HEARTBEAT); initializeHeader(heartbeat.getHeader()); heartbeat.getHeader().setInt(MsgSeqNum.FIELD, i); messages.add(heartbeat.toString()); } } else { throw e; } } int msgSeqNum = 0; int begin = 0; int current = beginSeqNo; for (final String message : messages) { final Message msg = parseMessage(message); msgSeqNum = msg.getHeader().getInt(MsgSeqNum.FIELD); if ((current != msgSeqNum) && begin == 0) { begin = current; } final String msgType = msg.getHeader().getString(MsgType.FIELD); if (MessageUtils.isAdminMessage(msgType) && !forceResendWhenCorruptedStore) { if (begin == 0) { begin = msgSeqNum; } } else { initializeResendFields(msg); if (resendApproved(msg)) { if (begin != 0) { generateSequenceReset(receivedMessage, begin, msgSeqNum); } getLog().onEvent("Resending Message: " + msgSeqNum); send(msg.toString()); begin = 0; } else { if (begin == 0) { begin = msgSeqNum; } } } current = msgSeqNum + 1; } if (enableNextExpectedMsgSeqNum) { if (begin != 0) { generateSequenceReset(receivedMessage, begin, msgSeqNum + 1); } else /* * I've added an else here as I managed to fail this without it in a unit examples, however the unit examples data * may not have been realistic to production on the other hand. * Apart from the else */ generateSequenceResetIfNeeded(receivedMessage, beginSeqNo, endSeqNo, msgSeqNum); } else { if (begin != 0) { generateSequenceReset(receivedMessage, begin, msgSeqNum + 1); } generateSequenceResetIfNeeded(receivedMessage, beginSeqNo, endSeqNo, msgSeqNum); } } private void generateSequenceResetIfNeeded(Message receivedMessage, int beginSeqNo, int endSeqNo, int msgSeqNum) throws IOException, FieldNotFound { if (endSeqNo > msgSeqNum) { endSeqNo = endSeqNo + 1; final int next = state.getNextSenderMsgSeqNum(); if (endSeqNo > next) { endSeqNo = next; } generateSequenceReset(receivedMessage, beginSeqNo, endSeqNo); } } private void nextQueued() throws FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException, InvalidMessage { while (nextQueued(getExpectedTargetNum())) { // continue } } private boolean nextQueued(int num) throws FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException, InvalidMessage { final Message msg = state.dequeue(num); if (msg != null) { getLog().onEvent("Processing queued message: " + num); final String msgType = msg.getHeader().getString(MsgType.FIELD); if (msgType.equals(MsgType.LOGON) || msgType.equals(MsgType.RESEND_REQUEST)) { state.incrNextTargetMsgSeqNum(); } else { // TODO SESSION Is it really necessary to convert the queued message to a string? next(msg.toString()); } return true; } return false; } private void next(String msg) throws InvalidMessage, FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException { try { next(parseMessage(msg)); } catch (final InvalidMessage e) { final String message = "Invalid message: " + e; if (MsgType.LOGON.equals(MessageUtils.getMessageType(msg))) { disconnect(message, true); } else { getLog().onErrorEvent(message); if (resetOrDisconnectIfRequired(null)) { return; } } throw e; } } private void doTargetTooHigh(Message msg) throws FieldNotFound, IOException, InvalidMessage { final Message.Header header = msg.getHeader(); final String beginString = header.getString(BeginString.FIELD); final int msgSeqNum = header.getInt(MsgSeqNum.FIELD); getLog().onErrorEvent( "MsgSeqNum too high, expecting " + getExpectedTargetNum() + " but received " + msgSeqNum + ": " + msg); // automatically reset or disconnect the session if we have a problem when the connector is running if (resetOrDisconnectIfRequired(msg)) { return; } state.enqueue(msgSeqNum, msg); getLog().onEvent("Enqueued at pos " + msgSeqNum + ": " + msg); if (state.isResendRequested()) { final int[] range = state.getResendRange(); if (!redundantResentRequestsAllowed && msgSeqNum >= range[0]) { getLog().onEvent( "Already sent ResendRequest FROM: " + range[0] + " TO: " + range[1] + ". Not sending another."); return; } } generateResendRequest(beginString, msgSeqNum); } private void generateResendRequest(String beginString, int msgSeqNum) { final int beginSeqNo = getExpectedTargetNum(); final int endSeqNo = msgSeqNum - 1; sendResendRequest(beginString, msgSeqNum, beginSeqNo, endSeqNo); } private void sendResendRequest(String beginString, int msgSeqNum, int beginSeqNo, int endSeqNo) { int lastEndSeqNoSent = resendRequestChunkSize == 0 ? endSeqNo : beginSeqNo + resendRequestChunkSize - 1; if (lastEndSeqNoSent > endSeqNo) { lastEndSeqNoSent = endSeqNo; } if (lastEndSeqNoSent == endSeqNo && !useClosedRangeForResend) { if (beginString.compareTo("FIX.4.2") >= 0) { endSeqNo = 0; } else if (beginString.compareTo("FIX.4.1") <= 0) { endSeqNo = 999999; } } else { endSeqNo = lastEndSeqNoSent; } final Message resendRequest = messageFactory.create(beginString, MsgType.RESEND_REQUEST); resendRequest.setInt(BeginSeqNo.FIELD, beginSeqNo); resendRequest.setInt(EndSeqNo.FIELD, endSeqNo); initializeHeader(resendRequest.getHeader()); sendRaw(resendRequest, 0); getLog().onEvent("Sent ResendRequest FROM: " + beginSeqNo + " TO: " + lastEndSeqNoSent); state.setResendRange(beginSeqNo, msgSeqNum - 1, resendRequestChunkSize == 0 ? 0 : lastEndSeqNoSent); } private boolean validatePossDup(Message msg) throws FieldNotFound, IOException { final Message.Header header = msg.getHeader(); final String msgType = header.getString(MsgType.FIELD); final Date sendingTime = header.getUtcTimeStamp(SendingTime.FIELD); if (!msgType.equals(MsgType.SEQUENCE_RESET)) { if (header.isSetField(OrigSendingTime.FIELD)) { final Date origSendingTime = header.getUtcTimeStamp(OrigSendingTime.FIELD); if (origSendingTime.compareTo(sendingTime) > 0) { generateReject(msg, SessionRejectReason.SENDINGTIME_ACCURACY_PROBLEM, 0); generateLogout(); return false; } } else { // QFJ-703 if (requiresOrigSendingTime) { generateReject(msg, SessionRejectReason.REQUIRED_TAG_MISSING, OrigSendingTime.FIELD); return false; } } } return true; } private boolean isTargetTooHigh(int sequence) throws IOException { return sequence > state.getNextTargetMsgSeqNum(); } /** * Outgoing Logon in response to Logon received * @param otherLogon the one we are responding to with a Logon (response) * @param expectedTargetNum value for 789 tag (used only if enabled in properties) * @throws FieldNotFound expected message field of Logon not present. */ private void generateLogon(Message otherLogon, int expectedTargetNum) throws FieldNotFound { final Message logon = messageFactory.create(sessionID.getBeginString(), MsgType.LOGON); logon.setInt(EncryptMethod.FIELD, EncryptMethod.NONE_OTHER); if (state.isResetReceived()) { logon.setBoolean(ResetSeqNumFlag.FIELD, true); } logon.setInt(HeartBtInt.FIELD, otherLogon.getInt(HeartBtInt.FIELD)); if (sessionID.isFIXT()) { logon.setField(senderDefaultApplVerID); } if (enableLastMsgSeqNumProcessed) { logon.getHeader().setInt(LastMsgSeqNumProcessed.FIELD, otherLogon.getHeader().getInt(MsgSeqNum.FIELD)); } initializeHeader(logon.getHeader()); if (enableNextExpectedMsgSeqNum) { getLog().onEvent("Responding to Logon request with tag 789=" + expectedTargetNum); logon.setInt(NextExpectedMsgSeqNum.FIELD, expectedTargetNum); state.setLastExpectedLogonNextSeqNum(expectedTargetNum); } else { getLog().onEvent("Responding to Logon request"); } sendRaw(logon, 0); state.setLogonSent(true); } /** * Send the message * @param message is the message to send * @param num is the seq num of the message to send, if 0, * @return */ private boolean sendRaw(Message message, int num) { // sequence number must be locked until application // callback returns since it may be effectively rolled // back if the callback fails. state.lockSenderMsgSeqNum(); try { boolean result = false; final Message.Header header = message.getHeader(); final String msgType = header.getString(MsgType.FIELD); initializeHeader(header); if (num > 0) { header.setInt(MsgSeqNum.FIELD, num); } if (enableLastMsgSeqNumProcessed) { if (!header.isSetField(LastMsgSeqNumProcessed.FIELD)) { header.setInt(LastMsgSeqNumProcessed.FIELD, getExpectedTargetNum() - 1); } } String messageString = null; if (message.isAdmin()) { try { application.toAdmin(context,message, sessionID); } catch (final Throwable t) { logApplicationException("toAdmin()", t); } if (msgType.equals(MsgType.LOGON)) { if (!state.isResetReceived()) { boolean resetSeqNumFlag = false; if (message.isSetField(ResetSeqNumFlag.FIELD)) { resetSeqNumFlag = message.getBoolean(ResetSeqNumFlag.FIELD); } if (resetSeqNumFlag) { resetState(); message.getHeader().setInt(MsgSeqNum.FIELD, getExpectedSenderNum()); } state.setResetSent(resetSeqNumFlag); } } messageString = message.toString(); if (msgType.equals(MsgType.LOGON) || msgType.equals(MsgType.LOGOUT) || msgType.equals(MsgType.RESEND_REQUEST) || msgType.equals(MsgType.SEQUENCE_RESET) || isLoggedOn()) { result = send(messageString); } } else { try { application.toApp(context, message, sessionID); } catch (final DoNotSend e) { return false; } catch (final Throwable t) { logApplicationException("toApp()", t); } messageString = message.toString(); if (isLoggedOn()) { result = send(messageString); } } if (num == 0) { final int msgSeqNum = header.getInt(MsgSeqNum.FIELD); if (persistMessages) { state.set(msgSeqNum, messageString); } state.incrNextSenderMsgSeqNum(); } return result; } catch (final IOException e) { logThrowable(getLog(), "Error Reading/Writing in MessageStore", e); return false; } catch (final FieldNotFound e) { logThrowable(state.getLog(), "Error accessing message fields", e); return false; } finally { state.unlockSenderMsgSeqNum(); } } private void resetState() { state.reset(); stateListener.onReset(); } /** * Send a message to a counterparty. Sequence numbers and information about the sender * and target identification will be added automatically (or overwritten if that * information already is present). * * The returned status flag is included for * compatibility with the JNI API but it's usefulness is questionable. * In QuickFIX/J, the message is transmitted using asynchronous network I/O so the boolean * only indicates the message was successfully queued for transmission. An error could still * occur before the message data is actually sent. * * @param message the message to send * @return a status flag indicating whether the write to the network layer was successful. * */ public boolean send(Message message) { message.getHeader().removeField(PossDupFlag.FIELD); message.getHeader().removeField(OrigSendingTime.FIELD); return sendRaw(message, 0); } private boolean send(String messageString) { getLog().onOutgoing(messageString); synchronized (responderSync) { if (!hasResponder()) { getLog().onEvent("No responder, not sending message: " + messageString); return false; } return getResponder().send(messageString); } } private boolean isCorrectCompID(String senderCompID, String targetCompID) { if (!checkCompID) { return true; } return sessionID.getSenderCompID().equals(targetCompID) && sessionID.getTargetCompID().equals(senderCompID); } /** * Set the data dictionary. (QF Compatibility) * * @deprecated * @param dataDictionary */ @Deprecated public void setDataDictionary(DataDictionary dataDictionary) { throw new UnsupportedOperationException( "Modification of session dictionary is not supported in QFJ"); } public DataDictionary getDataDictionary() { if (!sessionID.isFIXT()) { // For pre-FIXT sessions, the session data dictionary is the same as the application // data dictionary. return dataDictionaryProvider.getSessionDataDictionary(sessionID.getBeginString()); } else { throw new SessionException("No default data dictionary for FIXT 1.1 and newer"); } } public DataDictionaryProvider getDataDictionaryProvider() { return dataDictionaryProvider; } public SessionID getSessionID() { return sessionID; } /** * Predicate for determining if the session should be active at the current * time. * * @return true if session should be active, false otherwise. */ public boolean isSessionTime() { return sessionSchedule.isSessionTime(); } /** * Sets the timeout for waiting for a logon response. * @param seconds the timeout in seconds */ public void setLogonTimeout(int seconds) { state.setLogonTimeout(seconds); } /** * Sets the timeout for waiting for a logout response. * @param seconds the timeout in seconds */ public void setLogoutTimeout(int seconds) { state.setLogoutTimeout(seconds); } /** * Internal use by acceptor code. * * @param heartbeatInterval */ public void setHeartBeatInterval(int heartbeatInterval) { state.setHeartBeatInterval(heartbeatInterval); } public boolean getCheckCompID() { return checkCompID; } public int getLogonTimeout() { return state.getLogonTimeout(); } public int getLogoutTimeout() { return state.getLogoutTimeout(); } public boolean getRedundantResentRequestsAllowed() { return redundantResentRequestsAllowed; } public boolean getRefreshOnLogon() { return refreshMessageStoreAtLogon; } public boolean getResetOnDisconnect() { return resetOnDisconnect; } public boolean getResetOnLogout() { return resetOnLogout; } public boolean isLogonAlreadySent() { return state.isLogonAlreadySent(); } public boolean isLogonReceived() { return state.isLogonReceived(); } public boolean isLogonSendNeeded() { return state.isLogonSendNeeded(); } public boolean isLogonSent() { return state.isLogonSent(); } public boolean isLogonTimedOut() { return state.isLogonTimedOut(); } public boolean isLogoutReceived() { return state.isLogoutReceived(); } public boolean isLogoutSent() { return state.isLogoutSent(); } public boolean isLogoutTimedOut() { return state.isLogoutTimedOut(); } public boolean isUsingDataDictionary() { return dataDictionaryProvider != null; } public Date getStartTime() throws IOException { return state.getCreationTime(); } public double getTestRequestDelayMultiplier() { return state.getTestRequestDelayMultiplier(); } @Override public String toString() { String s = sessionID.toString(); try { s += "[in:" + state.getNextTargetMsgSeqNum() + ",out:" + state.getNextSenderMsgSeqNum() + "]"; } catch (final IOException e) { LogUtil.logThrowable(this, e.getMessage(), e); } return s; } public void addStateListener(SessionStateListener listener) { stateListeners.addListener(listener); } public void removeStateListener(SessionStateListener listener) { stateListeners.removeListener(listener); } /** * @return the default application version ID for messages sent from this session */ public ApplVerID getSenderDefaultApplicationVersionID() { return new ApplVerID(senderDefaultApplVerID.getValue()); } /** * @return the default application version ID for messages received by this session */ public ApplVerID getTargetDefaultApplicationVersionID() { return targetDefaultApplVerID.get(); } /** * Sets the default application version ID for messages received by this session. * This is called by the AcceptorIoHandler upon reception of a Logon message and * should not be called by user code. * @param applVerID */ public void setTargetDefaultApplicationVersionID(ApplVerID applVerID) { targetDefaultApplVerID.set(applVerID); } private static String extractNumber(String txt, int from) { String ret = ""; for (int i = from; i != txt.length(); ++i) { final char c = txt.charAt(i); if (c >= '0' && c <= '9') { ret += c; } else { if (ret.length() != 0) { break; } } } return ret.trim(); } protected static Integer extractExpectedSequenceNumber(String txt) { if (txt == null) { return null; } String keyword = "expecting"; int pos = txt.indexOf(keyword); if (pos < 0) { keyword = "expected"; pos = txt.indexOf("expected"); } if (pos < 0) { return null; } final int from = pos + keyword.length(); final String val = extractNumber(txt, from); if (val.length() == 0) { return null; } try { return Integer.valueOf(val); } catch (final NumberFormatException e) { return null; } } public void setIgnoreHeartBeatFailure(boolean ignoreHeartBeatFailure) { disableHeartBeatCheck = ignoreHeartBeatFailure; } public void setRejectInvalidMessage(boolean rejectInvalidMessage) { this.rejectInvalidMessage = rejectInvalidMessage; } public void setRejectMessageOnUnhandledException(boolean rejectMessageOnUnhandledException) { this.rejectMessageOnUnhandledException = rejectMessageOnUnhandledException; } public void setRequiresOrigSendingTime(boolean requiresOrigSendingTime) { this.requiresOrigSendingTime = requiresOrigSendingTime; } public void setForceResendWhenCorruptedStore(boolean forceResendWhenCorruptedStore) { this.forceResendWhenCorruptedStore = forceResendWhenCorruptedStore; } public boolean isAllowedForSession(InetAddress remoteInetAddress) { if (allowedRemoteAddresses == null || allowedRemoteAddresses.isEmpty()) { return true; } return allowedRemoteAddresses.contains(remoteInetAddress); } /** * Closes session resources. This is for internal use and should typically * not be called by an user application. */ public void close() throws IOException { closeIfCloseable(getLog()); closeIfCloseable(getStore()); } private void closeIfCloseable(Object resource) throws IOException { if (resource instanceof Closeable) { ((Closeable) resource).close(); } } }