From 465d0dfa6ed5aec088be81164305d77c2f0982c0 Mon Sep 17 00:00:00 2001 From: Philip Whitehouse Date: Tue, 2 Jan 2018 11:40:41 +0000 Subject: [PATCH] Changes from FlexTrade --- pom.xml | 26 +- quickfixj-core/pom.xml | 14 +- .../jmx/mbean/session/SessionAdmin.java | 2 +- .../src/main/java/quickfix/AbstractLog.java | 8 + .../src/main/java/quickfix/Application.java | 8 +- .../java/quickfix/ApplicationAdapter.java | 8 +- .../ApplicationFunctionalAdapter.java | 34 +- .../main/java/quickfix/CachedFileStore.java | 8 + .../src/main/java/quickfix/CompositeLog.java | 51 +- .../main/java/quickfix/DataDictionary.java | 270 ++--- .../java/quickfix/DataDictionaryProxy.java | 433 +++++++ .../DefaultDataDictionaryProvider.java | 4 +- .../DefaultDataDictionaryProviderHolder.java | 7 + .../java/quickfix/DefaultSessionFactory.java | 108 +- .../java/quickfix/DefaultSessionSchedule.java | 278 +++-- .../main/java/quickfix/ErrorEventReasons.java | 30 + .../main/java/quickfix/FieldConvertError.java | 4 + .../main/java/quickfix/FieldException.java | 2 +- .../src/main/java/quickfix/FieldMap.java | 39 +- .../src/main/java/quickfix/FileLog.java | 7 +- .../src/main/java/quickfix/FileStore.java | 8 + .../main/java/quickfix/FromAdminListener.java | 2 +- .../main/java/quickfix/FromAppListener.java | 2 +- .../src/main/java/quickfix/IMessage.java | 39 + .../IgnoredGarbledMessageListener.java | 5 + .../java/quickfix/IncorrectDataFormat.java | 2 +- .../main/java/quickfix/IncorrectTagValue.java | 17 - .../src/main/java/quickfix/JdbcLog.java | 10 +- .../src/main/java/quickfix/JdbcStore.java | 4 + .../src/main/java/quickfix/JdbcUtil.java | 6 +- .../src/main/java/quickfix/Log.java | 15 +- .../src/main/java/quickfix/LogUtil.java | 8 +- .../src/main/java/quickfix/MemoryStore.java | 6 +- .../src/main/java/quickfix/Message.java | 485 ++++++-- .../src/main/java/quickfix/MessageQueue.java | 2 +- .../src/main/java/quickfix/MessageStore.java | 9 + .../src/main/java/quickfix/MessageUtils.java | 37 +- .../src/main/java/quickfix/NoopStore.java | 6 + .../src/main/java/quickfix/SLF4JLog.java | 5 +- .../src/main/java/quickfix/ScreenLog.java | 7 +- .../src/main/java/quickfix/SendResult.java | 31 + .../src/main/java/quickfix/Session.java | 775 ++++++++---- .../src/main/java/quickfix/SessionID.java | 9 +- .../java/quickfix/SessionResendListener.java | 7 + .../main/java/quickfix/SessionSettings.java | 2 +- .../src/main/java/quickfix/SessionState.java | 20 +- .../java/quickfix/SessionStateListener.java | 2 +- .../src/main/java/quickfix/SimpleMessage.java | 266 +++++ .../main/java/quickfix/SleepycatStore.java | 353 ------ .../java/quickfix/SleepycatStoreFactory.java | 66 -- .../main/java/quickfix/SocketAcceptor.java | 1 + .../java/quickfix/ThreadedSocketAcceptor.java | 1 + .../src/main/java/quickfix/ToAppListener.java | 2 +- .../java/quickfix/ValidationSettings.java | 104 ++ .../java/quickfix/mina/AbstractIoHandler.java | 25 +- .../java/quickfix/mina/SessionConnector.java | 10 +- .../SingleThreadedEventHandlingStrategy.java | 4 +- ...ThreadPerSessionEventHandlingStrategy.java | 9 +- .../mina/acceptor/AbstractSocketAcceptor.java | 57 +- .../mina/acceptor/AcceptorIoHandler.java | 45 +- .../initiator/AbstractSocketInitiator.java | 4 +- .../mina/initiator/IoSessionInitiator.java | 24 +- .../mina/message/FIXMessageDecoder.java | 2 +- .../ApplicationFunctionalAdapterTest.java | 8 +- .../test/java/quickfix/CompositeLogTest.java | 103 +- .../quickfix/DataDictionaryProxyTest.java | 56 + .../java/quickfix/DataDictionaryTest.java | 426 ++++--- .../DefaultDataDictionaryProviderTest.java | 4 +- .../quickfix/DefaultSessionFactoryTest.java | 69 ++ .../quickfix/DefaultSessionScheduleTest.java | 379 ++++++ .../src/test/java/quickfix/FieldMapTest.java | 84 +- .../src/test/java/quickfix/FieldTest.java | 8 +- .../src/test/java/quickfix/FileLogTest.java | 2 +- .../src/test/java/quickfix/FileUtilTest.java | 23 - .../quickfix/IncorrectDataFormatTest.java | 27 + .../java/quickfix/IncorrectTagValueTest.java | 27 + .../src/test/java/quickfix/JdbcLogTest.java | 2 +- .../src/test/java/quickfix/LogUtilTest.java | 5 +- .../src/test/java/quickfix/LoginTestCase.java | 8 +- .../src/test/java/quickfix/MessageTest.java | 615 +++++++--- .../test/java/quickfix/MessageUtilsTest.java | 40 +- .../test/java/quickfix/MultiAcceptorTest.java | 5 +- .../quickfix/ParsingAndValidationTest.java | 114 ++ .../java/quickfix/RepeatingGroupTest.java | 60 +- .../src/test/java/quickfix/SLF4JLogTest.java | 10 +- .../test/java/quickfix/SerializationTest.java | 2 +- .../SessionDisconnectConcurrentlyTest.java | 5 +- .../java/quickfix/SessionDisconnectTest.java | 246 ++++ .../quickfix/SessionFactoryTestSupport.java | 54 +- .../src/test/java/quickfix/SessionIDTest.java | 9 +- .../test/java/quickfix/SessionStateTest.java | 4 +- .../src/test/java/quickfix/SessionTest.java | 1055 +++++++++++++---- .../java/quickfix/SessionTestSupport.java | 176 +++ .../test/java/quickfix/SessionUnregister.java | 22 + .../test/java/quickfix/Session_LogTest.java | 317 +++++ .../test/java/quickfix/SimpleMessageTest.java | 56 + .../java/quickfix/SleepycatStoreTest.java | 66 -- .../java/quickfix/SocketAcceptorTest.java | 4 +- .../java/quickfix/SocketInitiatorTest.java | 29 +- .../java/quickfix/UnitTestApplication.java | 16 +- .../java/quickfix/ValidationSettingsTest.java | 51 + .../java/quickfix/mina/LostLogoutTest.java | 35 +- .../quickfix/mina/LostLogoutThreadedTest.java | 35 +- .../mina/TestDataDictionaryProvider.java | 26 + ...adPerSessionEventHandlingStrategyTest.java | 31 +- .../mina/acceptor/AcceptorIoHandlerTest.java | 95 +- .../DynamicAcceptorSessionProviderTest.java | 28 +- .../initiator/InitiatorIoHandlerTest.java | 83 +- .../mina/message/FIXMessageDecoderTest.java | 3 +- .../test/acceptance/ATApplication.java | 24 +- .../acceptance/resynch/ResynchTestClient.java | 42 +- .../acceptance/resynch/ResynchTestServer.java | 29 +- quickfixj-distribution/pom.xml | 17 - .../examples/executor/Application.java | 2 +- .../quickfixj-messages-fix50sp2/pom.xml | 3 - 115 files changed, 6354 insertions(+), 2121 deletions(-) create mode 100644 quickfixj-core/src/main/java/quickfix/DataDictionaryProxy.java create mode 100644 quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProviderHolder.java create mode 100644 quickfixj-core/src/main/java/quickfix/ErrorEventReasons.java create mode 100644 quickfixj-core/src/main/java/quickfix/IMessage.java create mode 100644 quickfixj-core/src/main/java/quickfix/IgnoredGarbledMessageListener.java create mode 100644 quickfixj-core/src/main/java/quickfix/SendResult.java create mode 100644 quickfixj-core/src/main/java/quickfix/SessionResendListener.java create mode 100644 quickfixj-core/src/main/java/quickfix/SimpleMessage.java delete mode 100644 quickfixj-core/src/main/java/quickfix/SleepycatStore.java delete mode 100644 quickfixj-core/src/main/java/quickfix/SleepycatStoreFactory.java create mode 100644 quickfixj-core/src/main/java/quickfix/ValidationSettings.java create mode 100644 quickfixj-core/src/test/java/quickfix/DataDictionaryProxyTest.java create mode 100644 quickfixj-core/src/test/java/quickfix/DefaultSessionScheduleTest.java create mode 100644 quickfixj-core/src/test/java/quickfix/IncorrectDataFormatTest.java create mode 100644 quickfixj-core/src/test/java/quickfix/IncorrectTagValueTest.java create mode 100644 quickfixj-core/src/test/java/quickfix/ParsingAndValidationTest.java create mode 100644 quickfixj-core/src/test/java/quickfix/SessionDisconnectTest.java create mode 100644 quickfixj-core/src/test/java/quickfix/SessionTestSupport.java create mode 100644 quickfixj-core/src/test/java/quickfix/SessionUnregister.java create mode 100644 quickfixj-core/src/test/java/quickfix/Session_LogTest.java create mode 100644 quickfixj-core/src/test/java/quickfix/SimpleMessageTest.java delete mode 100644 quickfixj-core/src/test/java/quickfix/SleepycatStoreTest.java create mode 100644 quickfixj-core/src/test/java/quickfix/ValidationSettingsTest.java create mode 100644 quickfixj-core/src/test/java/quickfix/mina/TestDataDictionaryProvider.java diff --git a/pom.xml b/pom.xml index 25afb60fee..6dc781f233 100644 --- a/pom.xml +++ b/pom.xml @@ -232,6 +232,31 @@ org.apache.felix maven-bundle-plugin + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + org.jacoco + jacoco-maven-plugin + 0.8.7 + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + package + + report + + + + @@ -312,5 +337,4 @@ https://oss.sonatype.org/service/local/staging/deploy/maven2/ - diff --git a/quickfixj-core/pom.xml b/quickfixj-core/pom.xml index cd7c61e943..72854802e3 100644 --- a/quickfixj-core/pom.xml +++ b/quickfixj-core/pom.xml @@ -112,10 +112,16 @@ true - com.sleepycat - je - 18.3.12 - true + com.flextrade.jfixture + jfixture + 2.7.2 + test + + + org.mockito + mockito-all + + diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionAdmin.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionAdmin.java index 527325b060..977b7f8694 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionAdmin.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionAdmin.java @@ -137,7 +137,7 @@ public String getRemoteIPAddress() { */ public void reset() { logInvocation("reset"); - session.reset(); + session.reset("Reset requested"); } /* (non-Javadoc) diff --git a/quickfixj-core/src/main/java/quickfix/AbstractLog.java b/quickfixj-core/src/main/java/quickfix/AbstractLog.java index b86e08e538..e050729617 100644 --- a/quickfixj-core/src/main/java/quickfix/AbstractLog.java +++ b/quickfixj-core/src/main/java/quickfix/AbstractLog.java @@ -47,6 +47,14 @@ public final void onOutgoing(String message) { protected abstract void logOutgoing(String message); + @Override + public void onInvalidMessage(String messageString, String failureReason) { + } + + @Override + public void onDisconnect(String reason) { + } + public void close() throws IOException { // default is to do nothing } diff --git a/quickfixj-core/src/main/java/quickfix/Application.java b/quickfixj-core/src/main/java/quickfix/Application.java index 4d6379197f..bcbe4e6d9f 100644 --- a/quickfixj-core/src/main/java/quickfix/Application.java +++ b/quickfixj-core/src/main/java/quickfix/Application.java @@ -63,7 +63,7 @@ public interface Application { * @param message QuickFIX message * @param sessionId QuickFIX session ID */ - void toAdmin(Message message, SessionID sessionId); + void toAdmin(IMessage message, SessionID sessionId); /** * This callback notifies you when an administrative message is sent from a @@ -78,7 +78,7 @@ public interface Application { * @throws IncorrectTagValue * @throws RejectLogon causes a logon reject */ - void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, + void fromAdmin(IMessage message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon; /** @@ -97,7 +97,7 @@ void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, Incor * @param sessionId QuickFIX session ID * @throws DoNotSend This exception aborts message transmission */ - void toApp(Message message, SessionID sessionId) throws DoNotSend; + void toApp(IMessage message, SessionID sessionId) throws DoNotSend; /** * This callback receives messages for the application. This is one of the @@ -121,6 +121,6 @@ void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, Incor * @throws IncorrectTagValue * @throws UnsupportedMessageType */ - void fromApp(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, + void fromApp(IMessage message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType; } diff --git a/quickfixj-core/src/main/java/quickfix/ApplicationAdapter.java b/quickfixj-core/src/main/java/quickfix/ApplicationAdapter.java index 6aed00587c..34f9aca227 100644 --- a/quickfixj-core/src/main/java/quickfix/ApplicationAdapter.java +++ b/quickfixj-core/src/main/java/quickfix/ApplicationAdapter.java @@ -27,14 +27,14 @@ public class ApplicationAdapter implements Application { /* (non-Javadoc) * @see quickfix.Application#fromAdmin(quickfix.Message, quickfix.SessionID) */ - public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { + public void fromAdmin(IMessage message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { // EMPTY } /* (non-Javadoc) * @see quickfix.Application#fromApp(quickfix.Message, quickfix.SessionID) */ - public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { + public void fromApp(IMessage message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { // EMPTY } @@ -62,14 +62,14 @@ public void onLogout(SessionID sessionId) { /* (non-Javadoc) * @see quickfix.Application#toAdmin(quickfix.Message, quickfix.SessionID) */ - public void toAdmin(Message message, SessionID sessionId) { + public void toAdmin(IMessage message, SessionID sessionId) { // EMPTY } /* (non-Javadoc) * @see quickfix.Application#toApp(quickfix.Message, quickfix.SessionID) */ - public void toApp(Message message, SessionID sessionId) throws DoNotSend { + public void toApp(IMessage message, SessionID sessionId) throws DoNotSend { // EMPTY } diff --git a/quickfixj-core/src/main/java/quickfix/ApplicationFunctionalAdapter.java b/quickfixj-core/src/main/java/quickfix/ApplicationFunctionalAdapter.java index b4e970f79f..d4e08cd6c7 100644 --- a/quickfixj-core/src/main/java/quickfix/ApplicationFunctionalAdapter.java +++ b/quickfixj-core/src/main/java/quickfix/ApplicationFunctionalAdapter.java @@ -28,16 +28,16 @@ public class ApplicationFunctionalAdapter implements Application { private final List> onLogonListeners = new CopyOnWriteArrayList<>(); private final List> onLogoutListeners = new CopyOnWriteArrayList<>(); - private final List> toAdminListeners = new CopyOnWriteArrayList<>(); + private final List> toAdminListeners = new CopyOnWriteArrayList<>(); private final ConcurrentMap> toAdminTypeSafeListeners = new ConcurrentHashMap<>(); - private final List> fromAdminListeners = new CopyOnWriteArrayList<>(); + private final List> fromAdminListeners = new CopyOnWriteArrayList<>(); private final ConcurrentMap> fromAdminTypeSafeListeners = new ConcurrentHashMap<>(); - private final List> toAppListeners = new CopyOnWriteArrayList<>(); + private final List> toAppListeners = new CopyOnWriteArrayList<>(); private final ConcurrentMap> toAppTypeSafeListeners = new ConcurrentHashMap<>(); - private final List> fromAppListeners = new CopyOnWriteArrayList<>(); + private final List> fromAppListeners = new CopyOnWriteArrayList<>(); private final ConcurrentMap> fromAppTypeSafeListeners = new ConcurrentHashMap<>(); /** @@ -99,7 +99,7 @@ public void removeOnLogoutListener(Consumer onLogoutListener) { * * @param toAdminListener the BiConsumer of Session for toAdmin operation. */ - public void addToAdminListener(BiConsumer toAdminListener) { + public void addToAdminListener(BiConsumer toAdminListener) { toAdminListeners.add(toAdminListener); } @@ -109,7 +109,7 @@ public void addToAdminListener(BiConsumer toAdminListener) { * @param clazz the specific Message class the listener expects * @param toAdminListener the BiConsumer of Session for toAdmin operation. */ - public void addToAdminListener(Class clazz, BiConsumer toAdminListener) { + public void addToAdminListener(Class clazz, BiConsumer toAdminListener) { getList(toAdminTypeSafeListeners, clazz) .add(toAdminListener); } @@ -119,7 +119,7 @@ public void addToAdminListener(Class clazz, BiConsumer void removeToAdminListener(BiConsumer toAdminListener) { + public void removeToAdminListener(BiConsumer toAdminListener) { toAdminListeners.remove(toAdminListener); toAdminTypeSafeListeners .values() @@ -131,7 +131,7 @@ public void removeToAdminListener(BiConsumer t * * @param fromAdminListener the listener of fromAdmin operation. */ - public void addFromAdminListener(FromAdminListener fromAdminListener) { + public void addFromAdminListener(FromAdminListener fromAdminListener) { fromAdminListeners.add(fromAdminListener); } @@ -163,7 +163,7 @@ public void removeFromAdminListener(FromAdminListener fro * * @param toAppListener the listener of fromAdmin operation. */ - public void addToAppListener(ToAppListener toAppListener) { + public void addToAppListener(ToAppListener toAppListener) { toAppListeners.add(toAppListener); } @@ -195,7 +195,7 @@ public void removeToAppListener(ToAppListener toAppListen * * @param fromAppListener the listener of fromApp operation. */ - public void addFromAppListener(FromAppListener fromAppListener) { + public void addFromAppListener(FromAppListener fromAppListener) { fromAppListeners.add(fromAppListener); } @@ -238,15 +238,15 @@ public void onLogout(SessionID sessionId) { } @Override - public void toAdmin(Message message, SessionID sessionId) { + public void toAdmin(IMessage message, SessionID sessionId) { toAdminListeners.forEach(c -> c.accept(message, sessionId)); getList(toAdminTypeSafeListeners, message.getClass()) .forEach(c -> c.accept(message, sessionId)); } @Override - public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { - for (FromAdminListener listener : fromAdminListeners) { + public void fromAdmin(IMessage message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { + for (FromAdminListener listener : fromAdminListeners) { listener.accept(message, sessionId); } @@ -257,8 +257,8 @@ public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound } @Override - public void toApp(Message message, SessionID sessionId) throws DoNotSend { - for (ToAppListener listener : toAppListeners) { + public void toApp(IMessage message, SessionID sessionId) throws DoNotSend { + for (ToAppListener listener : toAppListeners) { listener.accept(message, sessionId); } @@ -268,8 +268,8 @@ public void toApp(Message message, SessionID sessionId) throws DoNotSend { } @Override - public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { - for (FromAppListener listener : fromAppListeners) { + public void fromApp(IMessage message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { + for (FromAppListener listener : fromAppListeners) { listener.accept(message, sessionId); } diff --git a/quickfixj-core/src/main/java/quickfix/CachedFileStore.java b/quickfixj-core/src/main/java/quickfix/CachedFileStore.java index 6ea5d01706..7a55920e3d 100644 --- a/quickfixj-core/src/main/java/quickfix/CachedFileStore.java +++ b/quickfixj-core/src/main/java/quickfix/CachedFileStore.java @@ -163,6 +163,14 @@ public Date getCreationTime() throws IOException { return cache.getCreationTime(); } + /* + * (non-Javadoc) + * @see quickfix.MessageStore#getCreationTimeCalendar() + */ + public Calendar getCreationTimeCalendar() throws IOException { + return cache.getCreationTimeCalendar(); + } + private void initializeSequenceNumbers() throws IOException { sequenceNumberFile.seek(0); if (sequenceNumberFile.length() > 0) { diff --git a/quickfixj-core/src/main/java/quickfix/CompositeLog.java b/quickfixj-core/src/main/java/quickfix/CompositeLog.java index 6c3547ed4d..bd70703374 100644 --- a/quickfixj-core/src/main/java/quickfix/CompositeLog.java +++ b/quickfixj-core/src/main/java/quickfix/CompositeLog.java @@ -36,59 +36,86 @@ public CompositeLog(Log[] logs) { this.logs = logs; } + @Override public void clear() { for (Log log : logs) { try { log.clear(); - } catch (Throwable e) { - handleError(e); + } catch (Exception e) { + handleException(e); } } } - private void handleError(Throwable e) { + private void handleException(Exception e) { if (rethrowException) { throw new RuntimeException(e); } defaultLog.error(e.getMessage() + ", continuing", e); } + @Override public void onIncoming(String message) { for (Log log : logs) { try { log.onIncoming(message); - } catch (Throwable e) { - handleError(e); + } catch (Exception e) { + handleException(e); } } } + @Override public void onOutgoing(String message) { for (Log log : logs) { try { log.onOutgoing(message); - } catch (Throwable e) { + } catch (Exception e) { defaultLog.error(e.getMessage() + ", continuing", e); } } } + @Override public void onEvent(String text) { for (Log log : logs) { try { log.onEvent(text); - } catch (Throwable e) { - handleError(e); + } catch (Exception e) { + handleException(e); } } } - public void onErrorEvent(String text) { + @Override + public void onErrorEvent(String category, String text) { for (Log log : logs) { try { - log.onErrorEvent(text); - } catch (Throwable e) { - handleError(e); + log.onErrorEvent(category, text); + } catch (Exception e) { + handleException(e); + } + } + } + + @Override + public void onInvalidMessage(String messageString, String failureReason) { + for (Log log : logs) { + try { + log.onInvalidMessage(messageString, failureReason); + } catch (Exception e) { + handleException(e); + } + } + } + + @Override + public void onDisconnect(String reason) { + for (Log log : logs) { + try { + log.onDisconnect(reason); + } catch (Exception e) { + handleException(e); } } } diff --git a/quickfixj-core/src/main/java/quickfix/DataDictionary.java b/quickfixj-core/src/main/java/quickfix/DataDictionary.java index d4113242c4..31de599745 100644 --- a/quickfixj-core/src/main/java/quickfix/DataDictionary.java +++ b/quickfixj-core/src/main/java/quickfix/DataDictionary.java @@ -24,6 +24,7 @@ import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; import quickfix.field.BeginString; import quickfix.field.MsgType; import quickfix.field.SessionRejectReason; @@ -49,6 +50,7 @@ import java.util.Set; import java.util.function.Supplier; import javax.xml.XMLConstants; +import javax.xml.parsers.ParserConfigurationException; import static quickfix.FileUtil.Location.CLASSLOADER_RESOURCE; import static quickfix.FileUtil.Location.CONTEXT_RESOURCE; @@ -88,11 +90,6 @@ private static Supplier createDocumentBuilderFactorySupp } private boolean hasVersion = false; - private boolean checkFieldsOutOfOrder = true; - private boolean checkFieldsHaveValues = true; - private boolean checkUserDefinedFields = true; - private boolean checkUnorderedGroupFields = true; - private boolean allowUnknownMessageFields = false; private String beginString; private String fullVersion; private String majorVersion; @@ -114,7 +111,7 @@ private static Supplier createDocumentBuilderFactorySupp private final Map components = new HashMap<>(); private int[] orderedFieldsArray; - private DataDictionary() { + protected DataDictionary() { } /** @@ -124,7 +121,7 @@ private DataDictionary() { * @throws ConfigError */ public DataDictionary(String location) throws ConfigError { - this(location, DEFAULT_DOCUMENT_BUILDER_FACTORY_SUPPLIER); + this(location, DEFAULT_DOCUMENT_BUILDER_FACTORY_SUPPLIER, true); } /** @@ -135,10 +132,19 @@ public DataDictionary(String location) throws ConfigError { * @throws ConfigError */ public DataDictionary(String location, Supplier documentBuilderFactorySupplier) throws - ConfigError { - read(location, documentBuilderFactorySupplier.get()); + ConfigError { + this(location, documentBuilderFactorySupplier, true); } + public DataDictionary(String location, boolean strictChecks) throws ConfigError { + this(location, DEFAULT_DOCUMENT_BUILDER_FACTORY_SUPPLIER, strictChecks); + } + + public DataDictionary(String location, Supplier documentBuilderFactorySupplier, boolean strictChecks) throws ConfigError { + read(location, documentBuilderFactorySupplier.get(), strictChecks); + } + + /** * Initialize a data dictionary from an input stream. * @@ -157,7 +163,11 @@ public DataDictionary(InputStream in) throws ConfigError { * @throws ConfigError */ public DataDictionary(InputStream in, Supplier documentBuilderFactorySupplier) throws ConfigError { - load(in, documentBuilderFactorySupplier.get()); + load(in, documentBuilderFactorySupplier.get(), true); + } + + public DataDictionary(InputStream in, boolean strictChecks) throws ConfigError { + load(in, DEFAULT_DOCUMENT_BUILDER_FACTORY_SUPPLIER.get(), strictChecks); } /** @@ -169,6 +179,10 @@ public DataDictionary(DataDictionary source) { copyFrom(source); } + public DataDictionary getUnderlyingDictionary() { + return this; + } + private void setVersion(String beginString) { this.beginString = beginString; hasVersion = true; @@ -346,7 +360,7 @@ public boolean isAppMessage(String msgType) { } private void addMsgField(String msgType, int field) { - messageFields.computeIfAbsent(msgType, k -> new HashSet<>()).add(field); + messageFields.computeIfAbsent(msgType, k -> new LinkedHashSet<>()).add(field); } /** @@ -397,7 +411,7 @@ public int getFieldTag(String name) { } private void addRequiredField(String msgType, int field) { - requiredFields.computeIfAbsent(msgType, k -> new HashSet<>()).add(field); + requiredFields.computeIfAbsent(msgType, k -> new LinkedHashSet<>()).add(field); } /** @@ -433,7 +447,7 @@ public boolean isRequiredTrailerField(int field) { } private void addFieldValue(int field, String value) { - fieldValues.computeIfAbsent(field, k -> new HashSet<>()).add(value); + fieldValues.computeIfAbsent(field, k -> new LinkedHashSet<>()).add(value); } /** @@ -532,86 +546,6 @@ private boolean isMultipleValueStringField(int field) { fieldType == FieldType.MULTIPLECHARVALUE; } - /** - * Controls whether out of order fields are checked. - * - * @param flag true = checked, false = not checked - */ - public void setCheckFieldsOutOfOrder(boolean flag) { - checkFieldsOutOfOrder = flag; - } - - public boolean isCheckFieldsOutOfOrder() { - return checkFieldsOutOfOrder; - } - - public boolean isCheckUnorderedGroupFields() { - return checkUnorderedGroupFields; - } - - public boolean isCheckFieldsHaveValues() { - return checkFieldsHaveValues; - } - - public boolean isCheckUserDefinedFields() { - return checkUserDefinedFields; - } - - public boolean isAllowUnknownMessageFields() { - return allowUnknownMessageFields; - } - - /** - * Controls whether group fields are in the same order - * - * @param flag true = checked, false = not checked - */ - public void setCheckUnorderedGroupFields(boolean flag) { - checkUnorderedGroupFields = flag; - for (Map gm : groups.values()) { - for (GroupInfo gi : gm.values()) { - gi.getDataDictionary().setCheckUnorderedGroupFields(flag); - } - } - } - - /** - * Controls whether empty field values are checked. - * - * @param flag true = checked, false = not checked - */ - public void setCheckFieldsHaveValues(boolean flag) { - checkFieldsHaveValues = flag; - for (Map gm : groups.values()) { - for (GroupInfo gi : gm.values()) { - gi.getDataDictionary().setCheckFieldsHaveValues(flag); - } - } - } - - /** - * Controls whether user defined fields are checked. - * - * @param flag true = checked, false = not checked - */ - public void setCheckUserDefinedFields(boolean flag) { - checkUserDefinedFields = flag; - for (Map gm : groups.values()) { - for (GroupInfo gi : gm.values()) { - gi.getDataDictionary().setCheckUserDefinedFields(flag); - } - } - } - - public void setAllowUnknownMessageFields(boolean allowUnknownFields) { - allowUnknownMessageFields = allowUnknownFields; - for (Map gm : groups.values()) { - for (GroupInfo gi : gm.values()) { - gi.getDataDictionary().setAllowUnknownMessageFields(allowUnknownFields); - } - } - } - private void copyFrom(DataDictionary rhs) { hasVersion = rhs.hasVersion; beginString = rhs.beginString; @@ -633,12 +567,6 @@ private void copyFrom(DataDictionary rhs) { copyGroups(groups, rhs.groups); copyMap(components, rhs.components); - setCheckFieldsOutOfOrder(rhs.checkFieldsOutOfOrder); - setCheckFieldsHaveValues(rhs.checkFieldsHaveValues); - setCheckUserDefinedFields(rhs.checkUserDefinedFields); - setCheckUnorderedGroupFields(rhs.checkUnorderedGroupFields); - setAllowUnknownMessageFields(rhs.allowUnknownMessageFields); - calculateOrderedFields(); } @@ -692,9 +620,9 @@ private static void copyCollection(Collection lhs, Collection rhs) { * @throws FieldNotFound if a field cannot be found * @throws IncorrectDataFormat if a field value has a wrong data type */ - public void validate(Message message) throws IncorrectTagValue, FieldNotFound, + public void validate(Message message, ValidationSettings settings) throws IncorrectTagValue, FieldNotFound, IncorrectDataFormat { - validate(message, false); + validate(message, false, settings); } /** @@ -706,13 +634,13 @@ public void validate(Message message) throws IncorrectTagValue, FieldNotFound, * @throws FieldNotFound if a field cannot be found * @throws IncorrectDataFormat if a field value has a wrong data type */ - public void validate(Message message, boolean bodyOnly) throws IncorrectTagValue, + public void validate(Message message, boolean bodyOnly, ValidationSettings settings) throws IncorrectTagValue, FieldNotFound, IncorrectDataFormat { - validate(message, bodyOnly ? null : this, this); + validate(message, bodyOnly ? null : getUnderlyingDictionary(), getUnderlyingDictionary(), settings); } static void validate(Message message, DataDictionary sessionDataDictionary, - DataDictionary applicationDataDictionary) throws IncorrectTagValue, FieldNotFound, + DataDictionary applicationDataDictionary, ValidationSettings settings) throws IncorrectTagValue, FieldNotFound, IncorrectDataFormat { final boolean bodyOnly = sessionDataDictionary == null; @@ -737,38 +665,38 @@ static void validate(Message message, DataDictionary sessionDataDictionary, } if (!bodyOnly) { - sessionDataDictionary.iterate(message.getHeader(), HEADER_ID, sessionDataDictionary); - sessionDataDictionary.iterate(message.getTrailer(), TRAILER_ID, sessionDataDictionary); + sessionDataDictionary.getUnderlyingDictionary().iterate(settings, message.getHeader(), HEADER_ID, sessionDataDictionary.getUnderlyingDictionary()); + sessionDataDictionary.getUnderlyingDictionary().iterate(settings, message.getTrailer(), TRAILER_ID, sessionDataDictionary.getUnderlyingDictionary()); } - applicationDataDictionary.iterate(message, msgType, applicationDataDictionary); + applicationDataDictionary.getUnderlyingDictionary().iterate(settings, message, msgType, applicationDataDictionary.getUnderlyingDictionary()); } private static boolean isVersionSpecified(DataDictionary dd) { - return dd != null && dd.hasVersion; + return dd != null && dd.getUnderlyingDictionary().hasVersion; } - private void iterate(FieldMap map, String msgType, DataDictionary dd) throws IncorrectTagValue, + private void iterate(ValidationSettings settings, FieldMap map, String msgType, DataDictionary dd) throws IncorrectTagValue, IncorrectDataFormat { for (final Field f : map) { final StringField field = (StringField) f; - checkHasValue(field); + checkHasValue(settings, field); if (hasVersion) { - checkValidFormat(field); + checkValidFormat(settings, field); checkValue(field); } if (beginString != null) { - dd.checkField(field, msgType, map instanceof Message); + dd.checkField(settings, field, msgType, map instanceof Message); dd.checkGroupCount(field, map, msgType); } } for (final List groups : map.getGroups().values()) { for (final Group group : groups) { - iterate(group, msgType, dd.getGroup(msgType, group.getFieldTag()) + iterate(settings, group, msgType, dd.getGroup(msgType, group.getFieldTag()) .getDataDictionary()); } } @@ -782,17 +710,17 @@ private void checkMsgType(String msgType) { } /** Check if field tag number is defined in spec. **/ - void checkValidTagNumber(Field field) { + public void checkValidTagNumber(Field field) { if (!fields.contains(field.getTag())) { throw new FieldException(SessionRejectReason.INVALID_TAG_NUMBER, field.getField()); } } /** Check if field tag is defined for message or group **/ - void checkField(Field field, String msgType, boolean message) { + public void checkField(ValidationSettings settings, Field field, String msgType, boolean message) { // use different validation for groups and messages boolean messageField = message ? isMsgField(msgType, field.getField()) : fields.contains(field.getField()); - boolean fail = checkFieldFailure(field.getField(), messageField); + boolean fail = checkFieldFailure(settings, field.getField(), messageField); if (fail) { if (fields.contains(field.getField())) { @@ -803,22 +731,22 @@ void checkField(Field field, String msgType, boolean message) { } } - boolean checkFieldFailure(int field, boolean messageField) { + public boolean checkFieldFailure(ValidationSettings settings, int field, boolean messageField) { boolean fail; if (field < USER_DEFINED_TAG_MIN) { - fail = !messageField && !allowUnknownMessageFields; + fail = !messageField && !settings.allowUnknownMessageFields; } else { - fail = !messageField && checkUserDefinedFields; + fail = !messageField && settings.checkUserDefinedFields; } return fail; } - private void checkValidFormat(StringField field) throws IncorrectDataFormat { + private void checkValidFormat(ValidationSettings settings, StringField field) throws IncorrectDataFormat { FieldType fieldType = getFieldType(field.getTag()); if (fieldType == null) { return; } - if (!checkFieldsHaveValues && field.getValue().length() == 0) { + if (!settings.checkFieldsHaveValues && field.getValue().length() == 0) { return; } try { @@ -878,13 +806,13 @@ private void checkValidFormat(StringField field) throws IncorrectDataFormat { private void checkValue(StringField field) throws IncorrectTagValue { int tag = field.getField(); if (hasFieldValue(tag) && !isFieldValue(tag, field.getValue())) { - throw new IncorrectTagValue(tag); + throw new IncorrectTagValue(tag, field.getValue()); } } /** Check if a field has a value. **/ - private void checkHasValue(StringField field) { - if (checkFieldsHaveValues && field.getValue().length() == 0) { + private void checkHasValue(ValidationSettings settings, StringField field) { + if (settings.checkFieldsHaveValues && field.getValue().length() == 0) { throw new FieldException(SessionRejectReason.TAG_SPECIFIED_WITHOUT_A_VALUE, field.getField()); } @@ -903,7 +831,7 @@ private void checkGroupCount(StringField field, FieldMap fieldMap, String msgTyp } /** Check if a message has all required fields. **/ - void checkHasRequired(FieldMap header, FieldMap body, FieldMap trailer, String msgType, + public void checkHasRequired(FieldMap header, FieldMap body, FieldMap trailer, String msgType, boolean bodyOnly) { if (!bodyOnly) { checkHasRequired(HEADER_ID, header, bodyOnly); @@ -951,7 +879,8 @@ private int countElementNodes(NodeList nodes) { return elementNodesCount; } - private void read(String location, DocumentBuilderFactory factory) throws ConfigError { + + private void read(String location, DocumentBuilderFactory factory, boolean strictChecks) throws ConfigError { final InputStream inputStream = FileUtil.open(getClass(), location, URL, FILESYSTEM, CONTEXT_RESOURCE, CLASSLOADER_RESOURCE); if (inputStream == null) { @@ -959,7 +888,7 @@ private void read(String location, DocumentBuilderFactory factory) throws Config } try { - load(inputStream, factory); + load(inputStream, factory, strictChecks); } catch (final Exception e) { throw new ConfigError(location + ": " + e.getMessage(), e); } finally { @@ -971,12 +900,12 @@ private void read(String location, DocumentBuilderFactory factory) throws Config } } - private void load(InputStream inputStream, DocumentBuilderFactory factory) throws ConfigError { + private void load(InputStream inputStream, DocumentBuilderFactory factory, boolean strictChecks) throws ConfigError { Document document; try { final DocumentBuilder builder = factory.newDocumentBuilder(); document = builder.parse(inputStream); - } catch (final Throwable e) { + } catch (final SAXException | IOException | ParserConfigurationException e) { throw new ConfigError("Could not parse data dictionary file", e); } @@ -1103,7 +1032,7 @@ private void load(InputStream inputStream, DocumentBuilderFactory factory) throw throw new ConfigError("
section not found in data dictionary"); } - load(document, HEADER_ID, headerNode.item(0)); + load(document, HEADER_ID, headerNode.item(0), strictChecks); // TRAILER final NodeList trailerNode = documentElement.getElementsByTagName("trailer"); @@ -1111,7 +1040,7 @@ private void load(InputStream inputStream, DocumentBuilderFactory factory) throw throw new ConfigError(" section not found in data dictionary"); } - load(document, TRAILER_ID, trailerNode.item(0)); + load(document, TRAILER_ID, trailerNode.item(0), strictChecks); } // MSGTYPE @@ -1145,7 +1074,7 @@ private void load(InputStream inputStream, DocumentBuilderFactory factory) throw addValueName(MsgType.FIELD, msgtype, name); } - load(document, msgtype, messageNode); + load(document, msgtype, messageNode, strictChecks); } } @@ -1165,10 +1094,10 @@ public int getNumMessageCategories() { return messageCategory.size(); } - private void load(Document document, String msgtype, Node node) throws ConfigError { + private void load(Document document, String msgtype, Node node, boolean strictChecks) throws ConfigError { String name; final NodeList fieldNodes = node.getChildNodes(); - if (countElementNodes(fieldNodes) == 0) { + if (strictChecks && countElementNodes(fieldNodes) == 0) { throw new ConfigError("No fields found: msgType=" + msgtype); } @@ -1198,7 +1127,7 @@ private void load(Document document, String msgtype, Node node) throws ConfigErr if (required == null) { throw new ConfigError(" does not have a 'required' attribute"); } - addXMLComponentFields(document, fieldNode, msgtype, this, + addXMLComponentFields(document, fieldNode, msgtype, getUnderlyingDictionary(), required.equalsIgnoreCase("Y")); } if (fieldNode.getNodeName().equals("group")) { @@ -1206,7 +1135,7 @@ private void load(Document document, String msgtype, Node node) throws ConfigErr if (required == null) { throw new ConfigError(" does not have a 'required' attribute"); } - addXMLGroup(document, fieldNode, msgtype, this, required.equalsIgnoreCase("Y")); + addXMLGroup(document, fieldNode, msgtype, getUnderlyingDictionary(), required.equalsIgnoreCase("Y")); } } } @@ -1229,6 +1158,69 @@ private void calculateOrderedFields() { } } + //Re-added to 2.0.0-SNAPSHOT by FlexTrade + /** + * Concatenates the Integer Sets into a single int[], with the header followed + * by the fields and finally the trailer. + * @param fieldsCollection + * @param headerCollection + * @param trailerCollection + * @return + */ + private int[] getOrderedFieldsFrom(Set fieldsCollection, + Set headerCollection, Set trailerCollection) { + + if (fieldsCollection == null) { + fieldsCollection = new LinkedHashSet<>(); + } + if (headerCollection == null) { + headerCollection = new LinkedHashSet<>(); + } + if (trailerCollection == null) { + trailerCollection = new LinkedHashSet<>(); + } + + int[] fields = new int[fieldsCollection.size() + headerCollection.size() + trailerCollection.size()]; + Integer[] headerArray = headerCollection.toArray(new Integer[headerCollection.size()]); + Integer[] fieldsArray = fieldsCollection.toArray(new Integer[fieldsCollection.size()]); + Integer[] trailerArray = trailerCollection.toArray(new Integer[trailerCollection.size()]); + + int overallIndex = 0; + + for (; overallIndex < headerArray.length; overallIndex++) + fields[overallIndex] = headerArray[overallIndex].intValue(); + + for (int i = 0; i < fieldsArray.length; i++, overallIndex++) + fields[overallIndex] = fieldsArray[i].intValue(); + + for (int i = 0; i < trailerArray.length; i++, overallIndex++) + fields[overallIndex] = trailerArray[i].intValue(); + + return fields; + } + + /** + * Returns the required ordered fields for a message type, including the Header and Trailer. + * @param messageType + * @return + */ + public int[] getOrderedRequiredFieldsForMessage(String messageType){ + return getOrderedFieldsFrom( + requiredFields.get(messageType), requiredFields.get(HEADER_ID), requiredFields.get(TRAILER_ID)); + } + + /** + * Returns the ordered fields for a message type, including the Header and Trailer. + * @param messageType + * @return + */ + public int[] getOrderedFieldsForMessage(String messageType){ + return getOrderedFieldsFrom( + messageFields.get(messageType), messageFields.get(HEADER_ID), messageFields.get(TRAILER_ID)); + } + + + private int lookupXMLFieldNumber(Document document, Node node) throws ConfigError { final Element element = (Element) node; if (!element.hasAttribute("name")) { @@ -1276,7 +1268,7 @@ private int addXMLComponentFields(Document document, Node node, String msgtype, } final String required = getAttribute(componentFieldNode, "required"); - if (required.equalsIgnoreCase("Y") && componentRequired) { + if (required != null && required.equalsIgnoreCase("Y") && componentRequired) { dd.addRequiredField(msgtype, field); } @@ -1285,13 +1277,13 @@ private int addXMLComponentFields(Document document, Node node, String msgtype, } if (componentFieldNode.getNodeName().equals("group")) { final String required = getAttribute(componentFieldNode, "required"); - final boolean isRequired = required.equalsIgnoreCase("Y"); + final boolean isRequired = required != null && required.equalsIgnoreCase("Y"); addXMLGroup(document, componentFieldNode, msgtype, dd, isRequired); } if (componentFieldNode.getNodeName().equals("component")) { final String required = getAttribute(componentFieldNode, "required"); - final boolean isRequired = required.equalsIgnoreCase("Y"); + final boolean isRequired = required != null && required.equalsIgnoreCase("Y"); addXMLComponentFields(document, componentFieldNode, msgtype, dd, isRequired); } } diff --git a/quickfixj-core/src/main/java/quickfix/DataDictionaryProxy.java b/quickfixj-core/src/main/java/quickfix/DataDictionaryProxy.java new file mode 100644 index 0000000000..dfbd91a87e --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/DataDictionaryProxy.java @@ -0,0 +1,433 @@ +//Added by FlexTrade + +package quickfix; + +/** + * Provide the message metadata for various versions of FIX. + */ +public class DataDictionaryProxy extends DataDictionary { + + private DataDictionary underlyingDictionary; + + public DataDictionaryProxy(DataDictionary underlyingDictionary) + throws ConfigError { + this.underlyingDictionary = underlyingDictionary; + } + + @Override + public final String toString() { + return "DictionaryProxy of " + underlyingDictionary.toString(); + } + + @Override + public DataDictionary getUnderlyingDictionary() { + return underlyingDictionary.getUnderlyingDictionary(); + } + + public void setUnderlyingDictionary(DataDictionary dataDictionary) { + this.underlyingDictionary = dataDictionary; + } + + @Override + public String getVersion() { + return underlyingDictionary.getVersion(); + } + + @Override + public String getFullVersion() { + return underlyingDictionary.getFullVersion(); + } + + @Override + public String getMajorVersion() { + return underlyingDictionary.getMajorVersion(); + } + + @Override + public int getMinorVersion() { + return underlyingDictionary.getMinorVersion(); + } + + @Override + public int getServicePack() { + return underlyingDictionary.getServicePack(); + } + + @Override + public int getExtensionPack() { + return underlyingDictionary.getExtensionPack(); + } + + @Override + public String getFieldName(int field) { + return underlyingDictionary.getFieldName(field); + } + + /** + * Get the value, if any, for an enumerated field value. + * + * @param field + * the tag + * @param value + * the value + * @return the value's name + */ + @Override + public String getValue(int field, String value) { + return underlyingDictionary.getValue(field, value); + } + + + /** + * Get the value name, if any, for an enumerated field value. + * + * @param field + * the tag + * @param value + * the value + * @return the value's name + */ + @Override + public String getValueName(int field, String value) { + return underlyingDictionary.getValueName(field, value); + } + + /** + * Predicate for determining if a tag is a defined field. + * + * @param field + * the tag + * @return true if the field is defined, false otherwise + */ + @Override + public boolean isField(int field) { + return underlyingDictionary.isField(field); + } + + /** + * Return the message type for the specified name. + * + * @param msgName + * The message name. + * @return the message type + */ + @Override + public String getMsgType(String msgName) { + return underlyingDictionary.getMsgType(msgName); + } + + /** + * Predicate for determining if message type is valid for a specified FIX + * version. + * + * @param msgType + * the message type value + * @return true if the message type if defined, false otherwise + */ + @Override + public boolean isMsgType(String msgType) { + return underlyingDictionary.isMsgType(msgType); + } + + /** + * Predicate for determining if a message is in the admin category. + * + * @param msgType + * the messageType + * @return true, if the msgType is a AdminMessage false, if the msgType is a + * ApplicationMessage + */ + @Override + public boolean isAdminMessage(String msgType) { + return underlyingDictionary.isAdminMessage(msgType); + } + + /** + * Predicate for determining if a message is in the app category. + * + * @param msgType + * the messageType + * @return true, if the msgType is a ApplicationMessage false, if the + * msgType is a AdminMessage + */ + @Override + public boolean isAppMessage(String msgType) { + return underlyingDictionary.isAppMessage(msgType); + } + + /** + * Predicate for determining if a field is valid for a given message type. + * + * @param msgType + * the message type + * @param field + * the tag + * @return true if field is defined for message, false otherwise. + */ + @Override + public boolean isMsgField(String msgType, int field) { + return underlyingDictionary.isMsgField(msgType, field); + } + + /** + * Predicate for determining if field is a header field. + * + * @param field + * the tag + * @return true if field is a header field, false otherwise. + */ + @Override + public boolean isHeaderField(int field) { + return underlyingDictionary.isHeaderField(field); + } + + /** + * Predicate for determining if field is a trailer field. + * + * @param field + * the tag + * @return true if field is a trailer field, false otherwise. + */ + @Override + public boolean isTrailerField(int field) { + return underlyingDictionary.isTrailerField(field); + } + + /** + * Get the field type for a field. + * + * @param field + * a tag + * @return the field type + */ + @Override + public FieldType getFieldType(int field) { + return underlyingDictionary.getFieldType(field); + } + + /** + * Get the field tag given a field name. + * + * @param name + * the field name + * @return the tag + */ + @Override + public int getFieldTag(String name) { + return underlyingDictionary.getFieldTag(name); + } + + /** + * Predicate for determining if a field is required for a message type + * + * @param msgType + * the message type + * @param field + * the tag + * @return true if field is required, false otherwise + */ + @Override + public boolean isRequiredField(String msgType, int field) { + return underlyingDictionary.isRequiredField(msgType, field); + } + + /** + * Predicate for determining if a header field is a required field + * + * @param field + * the tag + * @return true if field s required, false otherwise + */ + @Override + public boolean isRequiredHeaderField(int field) { + return underlyingDictionary.isRequiredHeaderField(field); + } + + /** + * Predicate for determining if a trailer field is a required field + * + * @param field + * the tag + * @return true if field s required, false otherwise + */ + @Override + public boolean isRequiredTrailerField(int field) { + return underlyingDictionary.isRequiredTrailerField(field); + } + + /** + * Predicate for determining if a field has enumerated values. + * + * @param field + * the tag + * @return true if field is enumerated, false otherwise + */ + public boolean hasFieldValue(int field) { + return underlyingDictionary.hasFieldValue(field); + } + + /** + * Predicate for determining if a field value is valid + * + * @param field + * the tag + * @param value + * a possible field value + * @return true if field value is valid, false otherwise + */ + @Override + public boolean isFieldValue(int field, String value) { + return underlyingDictionary.isFieldValue(field, value); + } + + /** + * Predicate for determining if a field is a group count field for a message + * type. + * + * @param msg + * the message type + * @param field + * the tag + * @return true if field starts a repeating group, false otherwise + */ + @Override + public boolean isGroup(String msg, int field) { + return underlyingDictionary.isGroup(msg, field); + } + + /** + * Predicate for determining if a field is a header group count field + * + * @param field + * the tag + * @return true if field starts a repeating group, false otherwise + */ + @Override + public boolean isHeaderGroup(int field) { + return underlyingDictionary.isHeaderGroup(field); + } + + /** + * Get repeating group metadata. + * + * @param msg + * the message type + * @param field + * the tag + * @return an object containing group-related metadata + */ + @Override + public DataDictionary.GroupInfo getGroup(String msg, int field) { + return underlyingDictionary.getGroup(msg, field); + } + + /** + * Predicate for determining if a field is a FIX raw data field. + * + * @param field + * the tag + * @return true if field is a raw data field, false otherwise + */ + @Override + public boolean isDataField(int field) { + return underlyingDictionary.isDataField(field); + } + + /** + * Validate a mesasge, including the header and trailer fields. + * + * @param message + * the message + * @throws IncorrectTagValue + * if a field value is not valid + * @throws FieldNotFound + * if a field cannot be found + * @throws IncorrectDataFormat + */ + @Override + public void validate(Message message, ValidationSettings validationSettings) throws IncorrectTagValue, + FieldNotFound, IncorrectDataFormat { + underlyingDictionary.validate(message, validationSettings); + } + + /** + * Validate the message body, with header and trailer fields being validated + * conditionally. + * + * @param message + * the message + * @param bodyOnly + * whether to validate just the message body, or to validate the + * header and trailer sections as well. + * @throws IncorrectTagValue + * if a field value is not valid + * @throws FieldNotFound + * if a field cannot be found + * @throws IncorrectDataFormat + */ + @Override + public void validate(Message message, boolean bodyOnly, ValidationSettings validationSettings) + throws IncorrectTagValue, FieldNotFound, IncorrectDataFormat { + underlyingDictionary.validate(message, bodyOnly, validationSettings); + } + + // / Check if field tag number is defined in spec. + @Override + public void checkValidTagNumber(Field field) { + underlyingDictionary.checkValidTagNumber(field); + } + + // / Check if field tag is defined for message or group + @Override + public void checkField(ValidationSettings validationSettings, Field field, String msgType, boolean message) { + underlyingDictionary.checkField(validationSettings, field, msgType, message); + } + + @Override + public boolean checkFieldFailure(ValidationSettings validationSettings, int field, boolean messageField) { + return underlyingDictionary.checkFieldFailure(validationSettings, field, messageField); + } + + // / Check if a message has all required fields. + @Override + public void checkHasRequired(FieldMap header, FieldMap body, FieldMap trailer, + String msgType, boolean bodyOnly) { + underlyingDictionary.checkHasRequired(header, body, trailer, msgType, + bodyOnly); + } + + @Override + public int getNumMessageCategories() { + return underlyingDictionary.getNumMessageCategories(); + } + + @Override + public int[] getOrderedFields() { + return underlyingDictionary.getOrderedFields(); + } + + /** + * Returns the required ordered fields for a message type, including the + * Header and Trailer. + * + * @param messageType + * @return + */ + @Override + public int[] getOrderedRequiredFieldsForMessage(String messageType) { + return underlyingDictionary + .getOrderedRequiredFieldsForMessage(messageType); + } + + /** + * Returns the ordered fields for a message type, including the Header and + * Trailer. + * + * @param messageType + * @return + */ + @Override + public int[] getOrderedFieldsForMessage(String messageType) { + return underlyingDictionary.getOrderedFieldsForMessage(messageType); + } +} diff --git a/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProvider.java b/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProvider.java index 63eb353b86..b1959a5991 100644 --- a/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProvider.java +++ b/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProvider.java @@ -38,7 +38,7 @@ public DefaultDataDictionaryProvider(boolean findDataDictionaries) { if (findDataDictionaries) { final String path = beginString.replace(".", "") + ".xml"; try { - return new DataDictionary(path); + return new DataDictionary(path, true); } catch (ConfigError e) { throw new QFJException(e); } @@ -50,7 +50,7 @@ public DefaultDataDictionaryProvider(boolean findDataDictionaries) { final String beginString = toBeginString(applVerID); final String path = beginString.replace(".", "") + ".xml"; try { - return new DataDictionary(path); + return new DataDictionary(path, true); } catch (ConfigError e) { throw new QFJException(e); } diff --git a/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProviderHolder.java b/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProviderHolder.java new file mode 100644 index 0000000000..9c8d1e03a8 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProviderHolder.java @@ -0,0 +1,7 @@ +package quickfix; + +public class DefaultDataDictionaryProviderHolder { + public DefaultDataDictionaryProvider obtain() { + return new DefaultDataDictionaryProvider(); + } +} diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java index cd83b72206..1126aa9f17 100644 --- a/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java +++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Properties; import java.util.Set; +import java.util.stream.Collectors; /** * Factory for creating sessions. Used by the communications code (acceptors, @@ -39,7 +40,7 @@ public class DefaultSessionFactory implements SessionFactory { private static final SimpleCache dictionaryCache = new SimpleCache<>(path -> { try { - return new DataDictionary(path); + return new DataDictionary(path, true); } catch (ConfigError e) { throw new QFJException(e); } @@ -51,47 +52,47 @@ public class DefaultSessionFactory implements SessionFactory { private final LogFactory logFactory; private final MessageFactory messageFactory; private final SessionScheduleFactory sessionScheduleFactory; + private final DefaultDataDictionaryProviderHolder dataDictionaryProviderHolder; public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory, LogFactory logFactory) { - this.application = application; - this.messageStoreFactory = messageStoreFactory; - this.messageQueueFactory = new InMemoryMessageQueueFactory(); - this.logFactory = logFactory; - this.messageFactory = new DefaultMessageFactory(); - this.sessionScheduleFactory = new DefaultSessionScheduleFactory(); + this(application, messageStoreFactory, new InMemoryMessageQueueFactory(), logFactory, + new DefaultMessageFactory(), new DefaultSessionScheduleFactory(), new DefaultDataDictionaryProviderHolder()); } public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory, LogFactory logFactory, MessageFactory messageFactory) { - this.application = application; - this.messageStoreFactory = messageStoreFactory; - this.messageQueueFactory = new InMemoryMessageQueueFactory(); - this.logFactory = logFactory; - this.messageFactory = messageFactory; - this.sessionScheduleFactory = new DefaultSessionScheduleFactory(); + this(application, messageStoreFactory, new InMemoryMessageQueueFactory(), logFactory, messageFactory, + new DefaultSessionScheduleFactory(), new DefaultDataDictionaryProviderHolder()); } public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory, LogFactory logFactory, MessageFactory messageFactory, SessionScheduleFactory sessionScheduleFactory) { - this.application = application; - this.messageStoreFactory = messageStoreFactory; - this.messageQueueFactory = new InMemoryMessageQueueFactory(); - this.logFactory = logFactory; - this.messageFactory = messageFactory; - this.sessionScheduleFactory = sessionScheduleFactory; + this(application, messageStoreFactory, new InMemoryMessageQueueFactory(), logFactory, messageFactory, + sessionScheduleFactory, new DefaultDataDictionaryProviderHolder()); } + public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory, MessageQueueFactory messageQueueFactory, LogFactory logFactory, MessageFactory messageFactory, SessionScheduleFactory sessionScheduleFactory) { + this(application, messageStoreFactory, messageQueueFactory, logFactory, messageFactory, + sessionScheduleFactory, new DefaultDataDictionaryProviderHolder()); + } + + + public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory, + MessageQueueFactory messageQueueFactory, LogFactory logFactory, + MessageFactory messageFactory, SessionScheduleFactory sessionScheduleFactory, + DefaultDataDictionaryProviderHolder dataDictionaryProviderHolder) { this.application = application; this.messageStoreFactory = messageStoreFactory; this.messageQueueFactory = messageQueueFactory; this.logFactory = logFactory; this.messageFactory = messageFactory; this.sessionScheduleFactory = sessionScheduleFactory; + this.dataDictionaryProviderHolder = dataDictionaryProviderHolder; } public Session create(SessionID sessionID, SessionSettings settings) throws ConfigError { @@ -104,6 +105,10 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf final boolean validateChecksum = getSetting(settings, sessionID, Session.SETTING_VALIDATE_CHECKSUM, true); + final Message.WeakParsingMode weakParsingMode = Message.WeakParsingMode.valueOf( + settings.isSetting(sessionID, Session.SETTING_WEAK_PARSING_MODE) ? + settings.getString(sessionID, Session.SETTING_WEAK_PARSING_MODE) : "DISABLED"); + if (rejectGarbledMessage && !validateChecksum) { throw new ConfigError("Not possible to reject garbled message and process " + "messages with invalid checksum at the same time."); @@ -162,13 +167,14 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf DefaultDataDictionaryProvider dataDictionaryProvider = null; if (useDataDictionary) { - dataDictionaryProvider = new DefaultDataDictionaryProvider(); + dataDictionaryProvider = dataDictionaryProviderHolder.obtain(); if (sessionID.isFIXT()) { processFixtDataDictionaries(sessionID, settings, dataDictionaryProvider); } else { processPreFixtDataDictionary(sessionID, settings, dataDictionaryProvider); } } + ValidationSettings validationSettings = createDataDictionarySettings(sessionID, settings); int heartbeatInterval = 0; if (connectionType.equals(SessionFactory.INITIATOR_CONNECTION_TYPE)) { @@ -219,6 +225,9 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf final int logonTimeout = getSetting(settings, sessionID, Session.SETTING_LOGON_TIMEOUT, 10); final int logoutTimeout = getSetting(settings, sessionID, Session.SETTING_LOGOUT_TIMEOUT, 2); + final boolean allowPosDup = getSetting(settings, sessionID, Session.SETTING_ALLOW_POS_DUP_MESSAGES, false); + final boolean fix41ResendRequestAsFix42 = getSetting(settings, sessionID, Session.SETTING_FIX_41_RESEND_AS_FIX42, false); + final boolean validateSequenceNumbers = getSetting(settings, sessionID, Session.SETTING_VALIDATE_SEQUENCE_NUMBERS, true); final boolean validateIncomingMessage = getSetting(settings, sessionID, Session.SETTING_VALIDATE_INCOMING_MESSAGE, true); final boolean resetOnError = getSetting(settings, sessionID, Session.SETTING_RESET_ON_ERROR, false); @@ -228,17 +237,22 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf final boolean enableNextExpectedMsgSeqNum = getSetting(settings, sessionID, Session.SETTING_ENABLE_NEXT_EXPECTED_MSG_SEQ_NUM, false); final boolean enableLastMsgSeqNumProcessed = getSetting(settings, sessionID, Session.SETTING_ENABLE_LAST_MSG_SEQ_NUM_PROCESSED, false); final int resendRequestChunkSize = getSetting(settings, sessionID, Session.SETTING_RESEND_REQUEST_CHUNK_SIZE, Session.DEFAULT_RESEND_RANGE_CHUNK_SIZE); - final boolean allowPossDup = getSetting(settings, sessionID, Session.SETTING_ALLOW_POS_DUP_MESSAGES, false); - + final boolean useDictionaryOrdering = getSetting(settings, sessionID, Session.SETTING_USE_DICTIONARY_ORDERING, false); final int[] logonIntervals = getLogonIntervalsInSeconds(settings, sessionID); final Set allowedRemoteAddresses = getInetAddresses(settings, sessionID); final SessionSchedule sessionSchedule = sessionScheduleFactory.create(sessionID, settings); final List logonTags = getLogonTags(settings, sessionID); + final int maxResendBatchRetrievalSize = getSetting(settings, sessionID, Session.SETTING_MAX_RESEND_RETRIEVAL_BATCH_SIZE, Session.DEFAULT_MAX_RESEND_BATCH_RETRIEVAL_SIZE); + String msgTypesToUseDictionaryString = settings.isSetting(sessionID, Session.SETTING_USE_DATA_DICTIONARY_FOR_MSG_TYPES) ? + settings.getString(sessionID, Session.SETTING_USE_DATA_DICTIONARY_FOR_MSG_TYPES) : ""; + final Set msgTypesToUseDictionary = toSet(msgTypesToUseDictionaryString); + + final boolean rejectMessageOutOfTime = getSetting(settings, sessionID, Session.SETTING_REJECT_MESSAGE_OUT_OF_TIME, true); final Session session = new Session(application, messageStoreFactory, messageQueueFactory, - sessionID, dataDictionaryProvider, sessionSchedule, logFactory, + sessionID, dataDictionaryProvider, validationSettings, sessionSchedule, logFactory, messageFactory, heartbeatInterval, checkLatency, maxLatency, timestampPrecision, resetOnLogon, resetOnLogout, resetOnDisconnect, refreshOnLogon, checkCompID, redundantResentRequestAllowed, persistMessages, useClosedIntervalForResend, @@ -247,10 +261,14 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf rejectInvalidMessage, rejectMessageOnUnhandledException, requiresOrigSendingTime, forceResendWhenCorruptedStore, allowedRemoteAddresses, validateIncomingMessage, resendRequestChunkSize, enableNextExpectedMsgSeqNum, enableLastMsgSeqNumProcessed, - validateChecksum, logonTags, heartBeatTimeoutMultiplier, allowPossDup); + validateChecksum, logonTags, heartBeatTimeoutMultiplier, useDictionaryOrdering, allowPosDup, + weakParsingMode, maxResendBatchRetrievalSize, msgTypesToUseDictionary, rejectMessageOutOfTime); session.setLogonTimeout(logonTimeout); session.setLogoutTimeout(logoutTimeout); + session.setAllowPosDup(allowPosDup); + session.setFix41ResendRequestAsFix42(fix41ResendRequestAsFix42); + session.setUseDictionaryOrdering(useDictionaryOrdering); final int maxScheduledWriteRequests = getSetting(settings, sessionID, Session.SETTING_MAX_SCHEDULED_WRITE_REQUESTS, 0); session.setMaxScheduledWriteRequests(maxScheduledWriteRequests); @@ -269,6 +287,10 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf } } + private Set toSet(String settingValue) { + return Arrays.stream(settingValue.split(",")).filter(v -> !v.isEmpty()).collect(Collectors.toSet()); + } + private void processPreFixtDataDictionary(SessionID sessionID, SessionSettings settings, DefaultDataDictionaryProvider dataDictionaryProvider) throws ConfigError, FieldConvertError { @@ -282,34 +304,48 @@ private void processPreFixtDataDictionary(SessionID sessionID, SessionSettings s private DataDictionary createDataDictionary(SessionID sessionID, SessionSettings settings, String settingsKey, String beginString) throws ConfigError, FieldConvertError { final String path = getDictionaryPath(sessionID, settings, settingsKey, beginString); - final DataDictionary dataDictionary = getDataDictionary(path); + return getDataDictionary(path); + } + + private ValidationSettings createDataDictionarySettings(SessionID sessionID, SessionSettings settings) throws FieldConvertError, ConfigError { + ValidationSettings validationSettings = new ValidationSettings(); if (settings.isSetting(sessionID, Session.SETTING_VALIDATE_FIELDS_OUT_OF_ORDER)) { - dataDictionary.setCheckFieldsOutOfOrder(settings.getBool(sessionID, + validationSettings.setCheckFieldsOutOfOrder(settings.getBool(sessionID, Session.SETTING_VALIDATE_FIELDS_OUT_OF_ORDER)); } if (settings.isSetting(sessionID, Session.SETTING_VALIDATE_FIELDS_HAVE_VALUES)) { - dataDictionary.setCheckFieldsHaveValues(settings.getBool(sessionID, + validationSettings.setCheckFieldsHaveValues(settings.getBool(sessionID, Session.SETTING_VALIDATE_FIELDS_HAVE_VALUES)); } if (settings.isSetting(sessionID, Session.SETTING_VALIDATE_UNORDERED_GROUP_FIELDS)) { - dataDictionary.setCheckUnorderedGroupFields(settings.getBool(sessionID, + validationSettings.setCheckUnorderedGroupFields(settings.getBool(sessionID, Session.SETTING_VALIDATE_UNORDERED_GROUP_FIELDS)); } if (settings.isSetting(sessionID, Session.SETTING_VALIDATE_USER_DEFINED_FIELDS)) { - dataDictionary.setCheckUserDefinedFields(settings.getBool(sessionID, + validationSettings.setCheckUserDefinedFields(settings.getBool(sessionID, Session.SETTING_VALIDATE_USER_DEFINED_FIELDS)); } if (settings.isSetting(sessionID, Session.SETTING_ALLOW_UNKNOWN_MSG_FIELDS)) { - dataDictionary.setAllowUnknownMessageFields(settings.getBool(sessionID, + validationSettings.setAllowUnknownMessageFields(settings.getBool(sessionID, Session.SETTING_ALLOW_UNKNOWN_MSG_FIELDS)); } - return dataDictionary; + if (settings.isSetting(sessionID, Session.SETTING_USE_FIRST_TAG_AS_GROUP_DELIMITER)) { + validationSettings.setUseFirstTagAsGroupDelimiter(settings.getBool(sessionID, + Session.SETTING_ALLOW_UNKNOWN_MSG_FIELDS)); + } + + if (settings.isSetting(sessionID, Session.SETTING_ONLY_ALLOW_SEEN_OR_KNOWN_FIELDS_IN_LAST_GROUP)) { + validationSettings.setOnlyAllowSeenOrKnownFieldsInLastGroup(settings.getBool(sessionID, + Session.SETTING_ONLY_ALLOW_SEEN_OR_KNOWN_FIELDS_IN_LAST_GROUP)); + } + + return validationSettings; } private void processFixtDataDictionaries(SessionID sessionID, SessionSettings settings, @@ -324,7 +360,9 @@ private void processFixtDataDictionaries(SessionID sessionID, SessionSettings se final Enumeration keys = sessionProperties.propertyNames(); while (keys.hasMoreElements()) { final String key = (String) keys.nextElement(); - if (key.startsWith(Session.SETTING_APP_DATA_DICTIONARY)) { + if (key.equals(Session.SETTING_DATA_DICTIONARY)) { + throw new ConfigError("DataDictionary setting not supported in FIXT sessions"); + } else if (key.startsWith(Session.SETTING_APP_DATA_DICTIONARY)) { if (key.equals(Session.SETTING_APP_DATA_DICTIONARY)) { final ApplVerID applVerID = toApplVerID(settings.getString(sessionID, Session.SETTING_DEFAULT_APPL_VER_ID)); @@ -377,7 +415,7 @@ private String toDictionaryPath(String beginString) { return beginString.replaceAll("\\.", "") + ".xml"; } - private DataDictionary getDataDictionary(String path) throws ConfigError { + protected DataDictionary getDataDictionary(String path) throws ConfigError { try { return dictionaryCache.computeIfAbsent(path); } catch (QFJException e) { @@ -396,7 +434,7 @@ private int[] getLogonIntervalsInSeconds(SessionSettings settings, SessionID ses final int[] ret = SessionSettings.parseSettingReconnectInterval(raw); if (ret != null) return ret; - } catch (final Throwable e) { + } catch (final ConfigError e) { throw new ConfigError(e); } } @@ -410,7 +448,7 @@ private Set getInetAddresses(SessionSettings settings, SessionID se final String raw = settings.getString(sessionID, Session.SETTING_ALLOWED_REMOTE_ADDRESSES); return SessionSettings.parseRemoteAddresses(raw); - } catch (final Throwable e) { + } catch (final ConfigError e) { throw new ConfigError(e); } } diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java index 83d8bb5eab..9be565fb6f 100644 --- a/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java +++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java @@ -22,44 +22,80 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.sql.Time; import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.TimeZone; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Corresponds to SessionTime in C++ code */ -public class DefaultSessionSchedule implements SessionSchedule { +public class DefaultSessionSchedule implements SessionSchedule { + private enum SessionType { + NON_STOP, TIME_PERIODS, WEEKDAY, DAILY, WEEKLY + } + + private final SessionType sessionType; private static final int NOT_SET = -1; private static final Pattern TIME_PATTERN = Pattern.compile("(\\d{2}):(\\d{2}):(\\d{2})(.*)"); - private final TimeEndPoint startTime; - private final TimeEndPoint endTime; - private final boolean isNonStopSession; - private final boolean isWeekdaySession; private final int[] weekdayOffsets; + + private final List timePeriods; + protected static final Logger LOG = LoggerFactory.getLogger(DefaultSessionSchedule.class); + //Cache recent time data to reduce creation of calendar objects + private final ThreadLocal threadLocalCalendar; + private final ThreadLocal threadLocalRecentTimeInterval; + public DefaultSessionSchedule(SessionSettings settings, SessionID sessionID) throws ConfigError, FieldConvertError { + List timePeriodsData = new ArrayList<>(); + int[] weekdays = new int[]{}; + threadLocalCalendar = ThreadLocal.withInitial(SystemTime::getUtcCalendar); + threadLocalRecentTimeInterval = new ThreadLocal<>(); + + boolean isNonStopSession = settings.isSetting(sessionID, Session.SETTING_NON_STOP_SESSION) + && settings.getBool(sessionID, Session.SETTING_NON_STOP_SESSION); + boolean startDayPresent = settings.isSetting(sessionID, Session.SETTING_START_DAY); + boolean endDayPresent = settings.isSetting(sessionID, Session.SETTING_END_DAY); - isNonStopSession = settings.isSetting(sessionID, Session.SETTING_NON_STOP_SESSION) && settings.getBool(sessionID, Session.SETTING_NON_STOP_SESSION); TimeZone defaultTimeZone = getDefaultTimeZone(settings, sessionID); if (isNonStopSession) { - isWeekdaySession = false; - weekdayOffsets = new int[0]; - startTime = endTime = new TimeEndPoint(NOT_SET, 0, 0, 0, defaultTimeZone); - return; + sessionType = SessionType.NON_STOP; + weekdays = new int[0]; + timePeriodsData = Collections.singletonList( + new TimePeriod( + new TimeEndPoint(NOT_SET, 0, 0, 0, defaultTimeZone), + new TimeEndPoint(NOT_SET, 0, 0, 0, defaultTimeZone) + ) + ); + } else if (settings.isSetting(sessionID, Session.SETTING_WEEKDAYS)) { + sessionType = SessionType.WEEKDAY; + } else if (settings.isSetting(sessionID, Session.SETTING_TIME_PERIODS)) { + sessionType = SessionType.TIME_PERIODS; + timePeriodsData = new ArrayList<>(); + String[] periods = settings.getString(sessionID, Session.SETTING_TIME_PERIODS).split(","); + for (String period : periods) { + String[] startEnd = period.split(">"); + String[] startDayTime = startEnd[0].split(" "); + String[] endDayTime = startEnd[1].split(" "); + timePeriodsData.add(new TimePeriod( + getDayTimeEndPoint(sessionID.toString(), defaultTimeZone, startDayTime[1], startDayTime[0]), + getDayTimeEndPoint(sessionID.toString(), defaultTimeZone, endDayTime[1], endDayTime[0]) + )); + } } else { - isWeekdaySession = settings.isSetting(sessionID, Session.SETTING_WEEKDAYS); + if (startDayPresent && endDayPresent) { + sessionType = SessionType.WEEKLY; + } else { + sessionType = SessionType.DAILY; + } } - boolean startDayPresent = settings.isSetting(sessionID, Session.SETTING_START_DAY); - boolean endDayPresent = settings.isSetting(sessionID, Session.SETTING_END_DAY); - - if (isWeekdaySession) { - if (startDayPresent || endDayPresent ) + if (sessionType == SessionType.WEEKDAY) { + if (startDayPresent || endDayPresent) throw new ConfigError("Session " + sessionID + ": usage of StartDay or EndDay is not compatible with setting " + Session.SETTING_WEEKDAYS); String weekdayNames = settings.getString(sessionID, Session.SETTING_WEEKDAYS); @@ -67,12 +103,15 @@ public DefaultSessionSchedule(SessionSettings settings, SessionID sessionID) thr throw new ConfigError("Session " + sessionID + ": " + Session.SETTING_WEEKDAYS + " is empty"); String[] weekdayNameArray = weekdayNames.split(","); - weekdayOffsets = new int[weekdayNameArray.length]; + weekdays = new int[weekdayNameArray.length]; for (int i = 0; i < weekdayNameArray.length; i++) { - weekdayOffsets[i] = DayConverter.toInteger(weekdayNameArray[i]); + weekdays[i] = DayConverter.toInteger(weekdayNameArray[i]); + } + } + if (sessionType != SessionType.TIME_PERIODS && sessionType != SessionType.NON_STOP) { + if (sessionType != SessionType.WEEKDAY) { + weekdays = new int[0]; } - } else { - weekdayOffsets = new int[0]; if (startDayPresent && !endDayPresent) { throw new ConfigError("Session " + sessionID + ": StartDay used without EndDay"); @@ -81,10 +120,14 @@ public DefaultSessionSchedule(SessionSettings settings, SessionID sessionID) thr if (endDayPresent && !startDayPresent) { throw new ConfigError("Session " + sessionID + ": EndDay used without StartDay"); } + timePeriodsData = Collections.singletonList(new TimePeriod( + getTimeEndPoint(settings, sessionID, defaultTimeZone, Session.SETTING_START_TIME, Session.SETTING_START_DAY), + getTimeEndPoint(settings, sessionID, defaultTimeZone, Session.SETTING_END_TIME, Session.SETTING_END_DAY) + )); } - startTime = getTimeEndPoint(settings, sessionID, defaultTimeZone, Session.SETTING_START_TIME, Session.SETTING_START_DAY); - endTime = getTimeEndPoint(settings, sessionID, defaultTimeZone, Session.SETTING_END_TIME, Session.SETTING_END_DAY); - LOG.info("[{}] {}", sessionID, toString()); + timePeriods = timePeriodsData; + weekdayOffsets = weekdays; + LOG.info("[{}] {}", sessionID, this); } private TimeEndPoint getTimeEndPoint(SessionSettings settings, SessionID sessionID, @@ -103,8 +146,22 @@ private TimeEndPoint getTimeEndPoint(SessionSettings settings, SessionID session Integer.parseInt(matcher.group(3)), getTimeZone(matcher.group(4), defaultTimeZone)); } + private TimeEndPoint getDayTimeEndPoint(String sessionID, TimeZone defaultTimeZone, String timeSetting, String daySetting) throws ConfigError, + FieldConvertError { + + Matcher matcher = TIME_PATTERN.matcher(timeSetting); + if (!matcher.find()) { + throw new ConfigError("Session " + sessionID + ": could not parse time '" + timeSetting + "'."); + } + + return new TimeEndPoint( + DayConverter.toInteger(daySetting), + Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)), + Integer.parseInt(matcher.group(3)), getTimeZone(matcher.group(4), defaultTimeZone)); + } + private TimeZone getDefaultTimeZone(SessionSettings settings, SessionID sessionID) - throws ConfigError, FieldConvertError { + throws ConfigError { TimeZone sessionTimeZone; if (settings.isSetting(sessionID, Session.SETTING_TIMEZONE)) { String sessionTimeZoneID = settings.getString(sessionID, Session.SETTING_TIMEZONE); @@ -177,6 +234,16 @@ TimeZone getTimeZone() { } } + private static class TimePeriod { + private final TimeEndPoint startTime; + private final TimeEndPoint endTime; + + private TimePeriod(TimeEndPoint startTime, TimeEndPoint endTime) { + this.startTime = startTime; + this.endTime = endTime; + } + } + /** * find the most recent session date/time range on or before t * if t is in a session then that session will be returned @@ -184,26 +251,42 @@ TimeZone getTimeZone() { * @return relevant session date/time range */ private TimeInterval theMostRecentIntervalBefore(Calendar t) { + switch (sessionType) { + case TIME_PERIODS: + TimeInterval interval = null; + for (int i = 0 ; i < timePeriods.size(); i++) { + TimeInterval nextInterval = theMostRecentIntervalBeforePeriod(t, timePeriods.get(i)); + if (interval == null || nextInterval.getStart().getTimeInMillis() > interval.getStart().getTimeInMillis()) { + interval = nextInterval; + } + } + return interval; + default: + return theMostRecentIntervalBeforePeriod(t, timePeriods.get(0)); + } + } + + private TimeInterval theMostRecentIntervalBeforePeriod(Calendar t, TimePeriod timePeriod) { TimeInterval timeInterval = new TimeInterval(); Calendar intervalStart = timeInterval.getStart(); - intervalStart.setTimeZone(startTime.getTimeZone()); + intervalStart.setTimeZone(timePeriod.startTime.getTimeZone()); intervalStart.setTimeInMillis(t.getTimeInMillis()); - intervalStart.set(Calendar.HOUR_OF_DAY, startTime.getHour()); - intervalStart.set(Calendar.MINUTE, startTime.getMinute()); - intervalStart.set(Calendar.SECOND, startTime.getSecond()); + intervalStart.set(Calendar.HOUR_OF_DAY, timePeriod.startTime.getHour()); + intervalStart.set(Calendar.MINUTE, timePeriod.startTime.getMinute()); + intervalStart.set(Calendar.SECOND, timePeriod.startTime.getSecond()); intervalStart.set(Calendar.MILLISECOND, 0); Calendar intervalEnd = timeInterval.getEnd(); - intervalEnd.setTimeZone(endTime.getTimeZone()); + intervalEnd.setTimeZone(timePeriod.endTime.getTimeZone()); intervalEnd.setTimeInMillis(t.getTimeInMillis()); - intervalEnd.set(Calendar.HOUR_OF_DAY, endTime.getHour()); - intervalEnd.set(Calendar.MINUTE, endTime.getMinute()); - intervalEnd.set(Calendar.SECOND, endTime.getSecond()); + intervalEnd.set(Calendar.HOUR_OF_DAY, timePeriod.endTime.getHour()); + intervalEnd.set(Calendar.MINUTE, timePeriod.endTime.getMinute()); + intervalEnd.set(Calendar.SECOND, timePeriod.endTime.getSecond()); intervalEnd.set(Calendar.MILLISECOND, 0); - if (isWeekdaySession) { + if (sessionType == SessionType.WEEKDAY) { while (intervalStart.getTimeInMillis() > t.getTimeInMillis() || - !validDayOfWeek(intervalStart)) { + !validDayOfWeek(intervalStart)) { intervalStart.add(Calendar.DAY_OF_WEEK, -1); intervalEnd.add(Calendar.DAY_OF_WEEK, -1); } @@ -211,10 +294,20 @@ private TimeInterval theMostRecentIntervalBefore(Calendar t) { if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) { intervalEnd.add(Calendar.DAY_OF_WEEK, 1); } + } else if (sessionType == SessionType.TIME_PERIODS) { + while (intervalStart.getTimeInMillis() > t.getTimeInMillis() || + !isDayOfWeek(intervalStart, timePeriod.startTime.getDay())) { + intervalStart.add(Calendar.DAY_OF_WEEK, -1); + } + + intervalEnd.set(Calendar.DAY_OF_WEEK, timePeriod.endTime.getDay()); + if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) { + intervalEnd.add(Calendar.WEEK_OF_MONTH, 1); + } } else { - if (isSet(startTime.getDay())) { - intervalStart.set(Calendar.DAY_OF_WEEK, startTime.getDay()); + if (isSet(timePeriod.startTime.getDay())) { + intervalStart.set(Calendar.DAY_OF_WEEK, timePeriod.startTime.getDay()); if (intervalStart.getTimeInMillis() > t.getTimeInMillis()) { intervalStart.add(Calendar.WEEK_OF_YEAR, -1); intervalEnd.add(Calendar.WEEK_OF_YEAR, -1); @@ -224,8 +317,8 @@ private TimeInterval theMostRecentIntervalBefore(Calendar t) { intervalEnd.add(Calendar.DAY_OF_YEAR, -1); } - if (isSet(endTime.getDay())) { - intervalEnd.set(Calendar.DAY_OF_WEEK, endTime.getDay()); + if (isSet(timePeriod.endTime.getDay())) { + intervalEnd.set(Calendar.DAY_OF_WEEK, timePeriod.endTime.getDay()); if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) { intervalEnd.add(Calendar.WEEK_OF_MONTH, 1); } @@ -233,7 +326,6 @@ private TimeInterval theMostRecentIntervalBefore(Calendar t) { intervalEnd.add(Calendar.DAY_OF_WEEK, 1); } } - return timeInterval; } @@ -288,11 +380,11 @@ public boolean isSameSession(Calendar time1, Calendar time2) { @Override public boolean isNonStopSession() { - return isNonStopSession; + return sessionType == SessionType.NON_STOP; } private boolean isDailySession() { - return !isSet(startTime.getDay()) && !isSet(endTime.getDay()); + return timePeriods.size() == 1 && !isSet(timePeriods.get(0).startTime.getDay()) && !isSet(timePeriods.get(0).endTime.getDay()); } @Override @@ -300,9 +392,16 @@ public boolean isSessionTime() { if(isNonStopSession()) { return true; } - Calendar now = SystemTime.getUtcCalendar(); - TimeInterval interval = theMostRecentIntervalBefore(now); - return interval.isContainingTime(now); + Calendar now = threadLocalCalendar.get(); + now.setTimeInMillis(SystemTime.currentTimeMillis()); + TimeInterval mostRecentInterval = threadLocalRecentTimeInterval.get(); + if (mostRecentInterval != null && mostRecentInterval.isContainingTime(now)) { + return true; + } + mostRecentInterval = theMostRecentIntervalBefore(now); + boolean result = mostRecentInterval.isContainingTime(now); + threadLocalRecentTimeInterval.set(mostRecentInterval); + return result; } public String toString() { @@ -314,53 +413,81 @@ public String toString() { SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss-z"); timeFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - TimeInterval ti = theMostRecentIntervalBefore(SystemTime.getUtcCalendar()); + switch (sessionType) { + case DAILY: + buf.append("daily: "); + break; + case WEEKDAY: + buf.append("weekdays: "); + break; + case WEEKLY: + buf.append("weekly: "); + break; + case TIME_PERIODS: + buf.append("periods: "); + break; + case NON_STOP: + buf.append("non-stop"); + break; + } + + if (sessionType == SessionType.TIME_PERIODS) { + for (int i = 0; i < timePeriods.size(); i++) { + TimePeriod timePeriod = timePeriods.get(i); + if (i > 0) { + buf.append(", "); + } + formatTimePeriod(buf, timePeriod, timeFormat); + } + } else if (sessionType != SessionType.NON_STOP) { + if (sessionType == SessionType.WEEKDAY) { + for (int i = 0; i < weekdayOffsets.length; i++) { + formatDayOfWeek(buf, weekdayOffsets[i]); + buf.append(", "); + } + } + formatTimePeriod(buf, timePeriods.get(0), timeFormat); + } - formatTimeInterval(buf, ti, timeFormat, false); + return buf.toString(); + } - // Now the localized equivalents, if necessary - if (!startTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE) - || !endTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE)) { + private void formatTimePeriod(StringBuilder buf, TimePeriod timePeriod, SimpleDateFormat timeFormat) { + TimeInterval ti = theMostRecentIntervalBeforePeriod(SystemTime.getUtcCalendar(), timePeriod); + formatTimeInterval(buf, ti, timeFormat, timePeriod.startTime.getDay(), timePeriod.endTime.getDay(),false); + if (!timePeriods.get(0).startTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE) + || !timePeriods.get(0).endTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE)) { buf.append(" ("); - formatTimeInterval(buf, ti, timeFormat, true); + formatTimeInterval(buf, ti, timeFormat, timePeriod.startTime.getDay(), timePeriod.endTime.getDay(),true); buf.append(")"); } - - return buf.toString(); } - private void formatTimeInterval(StringBuilder buf, TimeInterval timeInterval, - SimpleDateFormat timeFormat, boolean local) { - if (isWeekdaySession) { - try { - for (int i = 0; i < weekdayOffsets.length; i++) { - buf.append(DayConverter.toString(weekdayOffsets[i])); - buf.append(", "); - } - } catch (ConfigError ex) { - // this can't happen as these are created using DayConverter.toInteger - } - } else if (!isDailySession()) { - buf.append("weekly, "); - formatDayOfWeek(buf, startTime.getDay()); + private void formatTimeInterval(StringBuilder buf, + TimeInterval timeInterval, + SimpleDateFormat timeFormat, + int startDay, + int endDay, + boolean local) { + + if (!isDailySession()) { + formatDayOfWeek(buf, startDay); buf.append(" "); - } else { - buf.append("daily, "); } if (local) { - timeFormat.setTimeZone(startTime.getTimeZone()); + timeFormat.setTimeZone(timePeriods.get(0).startTime.getTimeZone()); } buf.append(timeFormat.format(timeInterval.getStart().getTime())); buf.append(" - "); if (!isDailySession()) { - formatDayOfWeek(buf, endTime.getDay()); + formatDayOfWeek(buf, endDay); buf.append(" "); } if (local) { - timeFormat.setTimeZone(endTime.getTimeZone()); + timeFormat.setTimeZone(timePeriods.get(0).endTime.getTimeZone()); } buf.append(timeFormat.format(timeInterval.getEnd().getTime())); } @@ -404,4 +531,9 @@ private boolean validDayOfWeek(Calendar startDateTime) { return true; return false; } + + private boolean isDayOfWeek(Calendar startDateTime, int day) { + int dow = startDateTime.get(Calendar.DAY_OF_WEEK); + return day == dow; + } } diff --git a/quickfixj-core/src/main/java/quickfix/ErrorEventReasons.java b/quickfixj-core/src/main/java/quickfix/ErrorEventReasons.java new file mode 100644 index 0000000000..cf90dec011 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/ErrorEventReasons.java @@ -0,0 +1,30 @@ +package quickfix; + +public class ErrorEventReasons { + public static final String MULTIPLE_LOGONS = "MULTIPLE_LOGONS"; + public static final String GET_NEXT_SEQ_NUM_FAILURE = "GET_NEXT_SEQ_NUM_FAILURE"; + public static final String GARBLED_MESSAGE = "GARBLED_MESSAGE"; + public static final String SKIPPING_INVALID_MESSAGE = "SKIPPING_INVALID_MESSAGE"; + public static final String LOGON_REJECTED = "LOGON_REJECTED"; + public static final String REQUIRED_FIELD_MISSING = "REQUIRED_FIELD_MISSING"; + public static final String DISCONNECT_FOLLOWING_ERROR = "DISCONNECT_FOLLOWING_ERROR"; + public static final String REJECTING_INVALID_MESSAGE = "REJECTING_INVALID_MESSAGE"; + public static final String INVALID_MESSAGE = "INVALID_MESSAGE"; + public static final String RESETTING_ON_ERROR = "RESETTING_ON_ERROR"; + public static final String MESSAGE_MISSING_MSGSEQNUM = "MESSAGE_MISSING_MSGSEQNUM"; + public static final String INVALID_SEQUENCE_RESET = "INVALID_SEQUENCE_RESET"; + public static final String VERIFY_FAILED = "VERIFY_FAILED"; + public static final String LOGON_REQUEST_FAILURE = "LOGON_REQUEST_FAILURE"; + public static final String SENDING_REJECT = "SENDING_REJECT"; + public static final String SENDING_BUSINESS_REJECT = "SENDING_BUSINESS_REJECT"; + public static final String RESEND_REQUEST_PARSER_FAILURE = "RESEND_REQUEST_PARSER_FAILURE"; + public static final String RESEND_REQUEST_SEND_FAILURE = "RESEND_REQUEST_SEND_FAILURE"; + public static final String CONNECTION_FAILED = "CONNECTION_FAILED"; + public static final String REFRESH_UNSUPPORTED = "REFRESH_UNSUPPORTED"; + public static final String FAILED_TO_QUEUE_MESSAGE = "FAILED_TO_QUEUE_MESSAGE"; + public static final String FAILED_TO_CREATE_SESSION = "FAILED_TO_CREATE_SESSION"; + public static final String APPLICATION_ERROR = "APPLICATION_ERROR"; + public static final String IO_ERROR = "IO_ERROR"; + public static final String MESSAGE_PROCESSOR_ERROR = "MESSAGE_PROCESSOR_ERROR"; + public static final String MESSAGE_DISPATCHER_ERROR = "MESSAGE_DISPATCHER_ERROR"; +} diff --git a/quickfixj-core/src/main/java/quickfix/FieldConvertError.java b/quickfixj-core/src/main/java/quickfix/FieldConvertError.java index 2aad21ccf6..21596e001c 100644 --- a/quickfixj-core/src/main/java/quickfix/FieldConvertError.java +++ b/quickfixj-core/src/main/java/quickfix/FieldConvertError.java @@ -27,4 +27,8 @@ public class FieldConvertError extends Exception { public FieldConvertError(String s) { super(s); } + + public FieldConvertError(String s, Throwable throwable) { + super(s, throwable); + } } diff --git a/quickfixj-core/src/main/java/quickfix/FieldException.java b/quickfixj-core/src/main/java/quickfix/FieldException.java index fc070de2e0..9319b6d26b 100644 --- a/quickfixj-core/src/main/java/quickfix/FieldException.java +++ b/quickfixj-core/src/main/java/quickfix/FieldException.java @@ -36,7 +36,7 @@ public FieldException(int sessionRejectReason, int field) { } public FieldException(int sessionRejectReason, String msg, int field) { - super(msg); + super(msg + (field != -1 ? ", field=" + field : "")); this.sessionRejectReason = sessionRejectReason; this.field = field; } diff --git a/quickfixj-core/src/main/java/quickfix/FieldMap.java b/quickfixj-core/src/main/java/quickfix/FieldMap.java index 5ad5e21527..67984470ca 100644 --- a/quickfixj-core/src/main/java/quickfix/FieldMap.java +++ b/quickfixj-core/src/main/java/quickfix/FieldMap.java @@ -49,9 +49,9 @@ public abstract class FieldMap implements Serializable, Iterable> { static final long serialVersionUID = -3193357271891865972L; - private final int[] fieldOrder; + private int[] fieldOrder; - protected final TreeMap> fields; + protected Map> fields; protected final TreeMap> groups = new TreeMap<>(); @@ -63,7 +63,9 @@ public abstract class FieldMap implements Serializable, Iterable> { */ protected FieldMap(int[] fieldOrder) { this.fieldOrder = fieldOrder; - fields = new TreeMap<>(fieldOrder != null ? new FieldOrderComparator() : null); + fields = (fieldOrder != null) ? + new TreeMap<>(new FieldOrderComparator()) : + new LinkedHashMap<>(); } protected FieldMap() { @@ -113,6 +115,18 @@ private static boolean isOrderedField(int field, int[] fieldOrder) { return indexOf(field, fieldOrder) > -1; } + public void setFieldOrder(int[] fieldOrder) { + if (fieldOrder != null) { + this.fieldOrder = fieldOrder; + Map> fields = new TreeMap<>(new FieldOrderComparator()); + fields.putAll(this.fields); + this.fields = fields; + } else { + this.fieldOrder = null; + this.fields = new LinkedHashMap<>(this.fields); + } + } + private class FieldOrderComparator implements Comparator, Serializable { static final long serialVersionUID = 3416006398018829270L; @@ -454,6 +468,7 @@ public Iterator> iterator() { } protected void initializeFrom(FieldMap source) { + fieldOrder = source.getFieldOrder(); fields.clear(); fields.putAll(source.fields); for (Entry> entry : source.groups.entrySet()) { @@ -697,6 +712,24 @@ public void removeGroup(int num, int field) { } } + /** + * Added by Flextrade - 09/2011

+ * This version of the method genuinely removes the group, + * rather than simply emptying it. + * @param field + * @param removeCount + */ + public void removeGroup(int field, boolean removeCount) { + if(!removeCount) { + removeGroup(field); + } else { + getGroups(field).clear(); + groups.remove(new Integer(field)); + removeField(field); + } + } + + public void removeGroup(int num, Group group) { removeGroup(num, group.getFieldTag()); } diff --git a/quickfixj-core/src/main/java/quickfix/FileLog.java b/quickfixj-core/src/main/java/quickfix/FileLog.java index ecd84fe5aa..b74e850b84 100644 --- a/quickfixj-core/src/main/java/quickfix/FileLog.java +++ b/quickfixj-core/src/main/java/quickfix/FileLog.java @@ -84,10 +84,12 @@ private void openLogStreams(boolean append) throws FileNotFoundException { events = new FileOutputStream(eventFileName, append); } + @Override protected void logIncoming(String message) { writeMessage(messages, messagesLock, message, false); } + @Override protected void logOutgoing(String message) { writeMessage(messages, messagesLock, message, false); } @@ -113,11 +115,13 @@ private void writeMessage(FileOutputStream stream, Object lock, String message, } } + @Override public void onEvent(String message) { writeMessage(events, eventsLock, message, true); } - public void onErrorEvent(String message) { + @Override + public void onErrorEvent(String category, String message) { writeMessage(events, eventsLock, message, true); } @@ -154,6 +158,7 @@ public void close() throws IOException { * Deletes the log files. Do not perform any log operations while performing * this operation. */ + @Override public void clear() { try { close(); diff --git a/quickfixj-core/src/main/java/quickfix/FileStore.java b/quickfixj-core/src/main/java/quickfix/FileStore.java index 542347a0e0..0ef9bdf546 100644 --- a/quickfixj-core/src/main/java/quickfix/FileStore.java +++ b/quickfixj-core/src/main/java/quickfix/FileStore.java @@ -153,6 +153,14 @@ public Date getCreationTime() throws IOException { return cache.getCreationTime(); } + /* (non-Javadoc) + * @see quickfix.MessageStore#getCreationTimeCalendar() + */ + @Override + public Calendar getCreationTimeCalendar() throws IOException { + return cache.getCreationTimeCalendar(); + } + private void initializeSequenceNumbers() throws IOException { senderSequenceNumberFile.seek(0); if (senderSequenceNumberFile.length() > 0) { diff --git a/quickfixj-core/src/main/java/quickfix/FromAdminListener.java b/quickfixj-core/src/main/java/quickfix/FromAdminListener.java index b5ff2da23d..4cca1879f1 100644 --- a/quickfixj-core/src/main/java/quickfix/FromAdminListener.java +++ b/quickfixj-core/src/main/java/quickfix/FromAdminListener.java @@ -1,5 +1,5 @@ package quickfix; -public interface FromAdminListener { +public interface FromAdminListener { void accept(T message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon; } diff --git a/quickfixj-core/src/main/java/quickfix/FromAppListener.java b/quickfixj-core/src/main/java/quickfix/FromAppListener.java index 9ba801d817..2303977de3 100644 --- a/quickfixj-core/src/main/java/quickfix/FromAppListener.java +++ b/quickfixj-core/src/main/java/quickfix/FromAppListener.java @@ -1,5 +1,5 @@ package quickfix; -public interface FromAppListener { +public interface FromAppListener { void accept(T message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType; } diff --git a/quickfixj-core/src/main/java/quickfix/IMessage.java b/quickfixj-core/src/main/java/quickfix/IMessage.java new file mode 100644 index 0000000000..974b85e910 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/IMessage.java @@ -0,0 +1,39 @@ +package quickfix; + +import java.time.LocalDateTime; + +public interface IMessage { + + String toRawString(); + + boolean isAdmin(); + String getHeaderString(int field) throws FieldNotFound; + int getHeaderInt(int field) throws FieldNotFound; + + void setHeaderString(int field, String value); + + void setString(int tag, String value); + + void setHeaderInt(int field, int value); + + void setInt(int tag, int value); + + void setHeaderUtcTimeStamp(int field, LocalDateTime localDateTime, UtcTimestampPrecision timestampPrecision); + + boolean isSetField(int field); + + boolean getBoolean(int field) throws FieldNotFound; + + boolean isSetHeaderField(int field); + + int getInt(int tag) throws FieldNotFound; + + String getString(int tag) throws FieldNotFound; + + void removeHeaderField(int field); + + /** + * Provides the first error found while parsing the message + * May indicate the resulting data is only a partial copy of the raw string **/ + FieldException getException(); +} diff --git a/quickfixj-core/src/main/java/quickfix/IgnoredGarbledMessageListener.java b/quickfixj-core/src/main/java/quickfix/IgnoredGarbledMessageListener.java new file mode 100644 index 0000000000..510a3cb789 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/IgnoredGarbledMessageListener.java @@ -0,0 +1,5 @@ +package quickfix; + +public interface IgnoredGarbledMessageListener { + void garbledMessageIgnored(SessionID sessionID, Message message); +} diff --git a/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java b/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java index b1ae253f4d..aee0036808 100644 --- a/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java +++ b/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java @@ -34,7 +34,7 @@ public class IncorrectDataFormat extends Exception implements HasFieldAndReason * @param data the incorrect data */ public IncorrectDataFormat(final int field, final String data) { - this(field, data, SessionRejectReasonText.getMessage(SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE) + ", field=" + field); + this(field, data, SessionRejectReasonText.getMessage(SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE) + ", field=" + field + ", value="+ data); } /** diff --git a/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java b/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java index 9558a9fbf7..7b62e4ea51 100644 --- a/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java +++ b/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java @@ -43,23 +43,6 @@ public IncorrectTagValue(int field, String value) { this.sessionRejectReason = SessionRejectReason.VALUE_IS_INCORRECT; } - public IncorrectTagValue(int field, String value, String message) { - super(message); - this.field = field; - this.value = value; - this.sessionRejectReason = SessionRejectReason.VALUE_IS_INCORRECT; - } - - @Override - public String toString() { - String str = super.toString(); - if (field != 0) - str += " field=" + field; - if (value != null) - str += " value=" + value; - return str; - } - @Override public int getField() { return field; diff --git a/quickfixj-core/src/main/java/quickfix/JdbcLog.java b/quickfixj-core/src/main/java/quickfix/JdbcLog.java index 308e37fade..3f0c9a1adb 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcLog.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcLog.java @@ -127,14 +127,17 @@ private String getDeleteItemsSql(String tableName) { return deleteItemsSqlCache.get(tableName); } + @Override public void onEvent(String value) { insert(eventTableName, value); } + @Override protected void logIncoming(String message) { insert(incomingMessagesTableName, message); } + @Override protected void logOutgoing(String message) { insert(outgoingMessagesTableName, message); } @@ -166,7 +169,7 @@ private void insert(String tableName, String value) { insert.execute(); } catch (SQLException e) { recursiveException = e; - LogUtil.logThrowable(sessionID, e.getMessage(), e); + LogUtil.logThrowable(sessionID, ErrorEventReasons.IO_ERROR, e.getMessage(), e); } finally { JdbcUtil.close(sessionID, insert); JdbcUtil.close(sessionID, connection); @@ -193,7 +196,7 @@ private void clearTable(String tableName) { setSessionIdParameters(statement, 1); statement.execute(); } catch (SQLException e) { - LogUtil.logThrowable(sessionID, e.getMessage(), e); + LogUtil.logThrowable(sessionID, ErrorEventReasons.IO_ERROR, e.getMessage(), e); } finally { JdbcUtil.close(sessionID, statement); JdbcUtil.close(sessionID, connection); @@ -217,7 +220,8 @@ private int setSessionIdParameters(PreparedStatement query, int offset) throws S extendedSessionIdSupported, defaultSessionIdPropertyValue); } - public void onErrorEvent(String text) { + @Override + public void onErrorEvent(String category, String text) { onEvent(text); } } diff --git a/quickfixj-core/src/main/java/quickfix/JdbcStore.java b/quickfixj-core/src/main/java/quickfix/JdbcStore.java index eb88e7986d..a1f901e4ed 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcStore.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcStore.java @@ -159,6 +159,10 @@ public Date getCreationTime() throws IOException { return cache.getCreationTime(); } + public Calendar getCreationTimeCalendar() throws IOException { + return cache.getCreationTimeCalendar(); + } + public int getNextSenderMsgSeqNum() throws IOException { return cache.getNextSenderMsgSeqNum(); } diff --git a/quickfixj-core/src/main/java/quickfix/JdbcUtil.java b/quickfixj-core/src/main/java/quickfix/JdbcUtil.java index 14ffc259e1..82861c00ae 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcUtil.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcUtil.java @@ -121,7 +121,7 @@ static void close(SessionID sessionID, Connection connection) { try { connection.close(); } catch (SQLException e) { - LogUtil.logThrowable(sessionID, e.getMessage(), e); + LogUtil.logThrowable(sessionID, ErrorEventReasons.IO_ERROR, e.getMessage(), e); } } } @@ -131,7 +131,7 @@ static void close(SessionID sessionID, PreparedStatement statement) { try { statement.close(); } catch (SQLException e) { - LogUtil.logThrowable(sessionID, e.getMessage(), e); + LogUtil.logThrowable(sessionID, ErrorEventReasons.IO_ERROR, e.getMessage(), e); } } } @@ -141,7 +141,7 @@ static void close(SessionID sessionID, ResultSet rs) { try { rs.close(); } catch (SQLException e) { - LogUtil.logThrowable(sessionID, e.getMessage(), e); + LogUtil.logThrowable(sessionID, ErrorEventReasons.IO_ERROR, e.getMessage(), e); } } } diff --git a/quickfixj-core/src/main/java/quickfix/Log.java b/quickfixj-core/src/main/java/quickfix/Log.java index c2b833e498..36469a5450 100644 --- a/quickfixj-core/src/main/java/quickfix/Log.java +++ b/quickfixj-core/src/main/java/quickfix/Log.java @@ -55,6 +55,19 @@ public interface Log { * * @param text the event description */ - void onErrorEvent(String text); + void onErrorEvent(String category, String text); + + /** + * Logs an invalid incoming message. + * + * @param messageString the unparsed message + * @param failureReason the failure reason + */ + void onInvalidMessage(String messageString, String failureReason); + + /** + * Logs the reason for a disconnect + */ + void onDisconnect(String messageString); } diff --git a/quickfixj-core/src/main/java/quickfix/LogUtil.java b/quickfixj-core/src/main/java/quickfix/LogUtil.java index 48df75b5e8..ff7afa94c8 100644 --- a/quickfixj-core/src/main/java/quickfix/LogUtil.java +++ b/quickfixj-core/src/main/java/quickfix/LogUtil.java @@ -38,7 +38,7 @@ public class LogUtil { * @param message error message * @param t the exception to log */ - public static void logThrowable(Log log, String message, Throwable t) { + public static void logThrowable(Log log,String category, String message, Throwable t) { final StringWriter stringWriter = new StringWriter(); final PrintWriter printWriter = new PrintWriter(stringWriter); printWriter.println(message); @@ -47,7 +47,7 @@ public static void logThrowable(Log log, String message, Throwable t) { printWriter.println("Cause: " + t.getCause().getMessage()); t.getCause().printStackTrace(printWriter); } - log.onErrorEvent(stringWriter.toString()); + log.onErrorEvent(category, stringWriter.toString()); } /** @@ -57,10 +57,10 @@ public static void logThrowable(Log log, String message, Throwable t) { * @param message the error message * @param t the exception to log */ - public static void logThrowable(SessionID sessionID, String message, Throwable t) { + public static void logThrowable(SessionID sessionID, String category, String message, Throwable t) { final Session session = Session.lookupSession(sessionID); if (session != null) { - logThrowable(session.getLog(), message, t); + logThrowable(session.getLog(), category, message, t); } else { // QFJ-335 // It's possible the session has been deregistered by the time diff --git a/quickfixj-core/src/main/java/quickfix/MemoryStore.java b/quickfixj-core/src/main/java/quickfix/MemoryStore.java index f102056516..b4dc3bc5a4 100644 --- a/quickfixj-core/src/main/java/quickfix/MemoryStore.java +++ b/quickfixj-core/src/main/java/quickfix/MemoryStore.java @@ -70,6 +70,10 @@ public Date getCreationTime() throws IOException { return creationTime.getTime(); } + public Calendar getCreationTimeCalendar() throws IOException { + return creationTime; + } + /* package */void setCreationTime(Calendar creationTime) { this.creationTime = creationTime; } @@ -114,7 +118,7 @@ public void refresh() throws IOException { final String text = "memory store does not support refresh!"; final Session session = sessionID != null ? Session.lookupSession(sessionID) : null; if (session != null) { - session.getLog().onErrorEvent("ERROR: " + text); + session.getLog().onErrorEvent(ErrorEventReasons.REFRESH_UNSUPPORTED, "ERROR: " + text); } else { LoggerFactory.getLogger(MemoryStore.class).error(text); } diff --git a/quickfixj-core/src/main/java/quickfix/Message.java b/quickfixj-core/src/main/java/quickfix/Message.java index c974021e03..efbaaa9af5 100644 --- a/quickfixj-core/src/main/java/quickfix/Message.java +++ b/quickfixj-core/src/main/java/quickfix/Message.java @@ -66,52 +66,77 @@ import javax.xml.transform.stream.StreamResult; import java.io.ByteArrayOutputStream; import java.text.DecimalFormat; -import java.util.List; +import java.time.LocalDateTime; +import java.util.*; /** * Represents a FIX message. */ -public class Message extends FieldMap { +public class Message extends FieldMap implements IMessage { static final long serialVersionUID = -3193357271891865972L; protected Header header; protected Trailer trailer = new Trailer(); + protected List> remainingHeaderFields = new ArrayList<>(); + protected List> remainingBodyFields = new ArrayList<>(); private volatile FieldException exception; + private boolean lengthAndChecksumShouldBeAutomaticallyUpdated = true; public Message() { initializeHeader(); } - protected Message(int[] fieldOrder) { + public Message(boolean lengthAndChecksumShouldBeAutomaticallyUpdated) { + initializeHeader(); + this.lengthAndChecksumShouldBeAutomaticallyUpdated = lengthAndChecksumShouldBeAutomaticallyUpdated; + } + + public Message(int[] fieldOrder) { + super(fieldOrder); + initializeHeader(); + } + + public Message(int[] fieldOrder, boolean lengthAndChecksumShouldBeAutomaticallyUpdated) { super(fieldOrder); initializeHeader(); + this.lengthAndChecksumShouldBeAutomaticallyUpdated = lengthAndChecksumShouldBeAutomaticallyUpdated; } public Message(String string) throws InvalidMessage { initializeHeader(); - fromString(string, null, true, true); + fromString(string, null, null, null, true, true, WeakParsingMode.DISABLED); } public Message(String string, boolean validate) throws InvalidMessage { initializeHeader(); - fromString(string, null, validate, true); + fromString(string, null, null, null, validate, true, WeakParsingMode.DISABLED); + } + + public Message(String string, boolean validate, WeakParsingMode weakParsing) throws InvalidMessage { + initializeHeader(); + fromString(string, null, null, null, validate, true, weakParsing); } - public Message(String string, DataDictionary dd) throws InvalidMessage { + public Message(String string, DataDictionary dd, ValidationSettings dds) throws InvalidMessage { initializeHeader(); - fromString(string, dd, true, true); + fromString(string, dd, dds, true, true); } - public Message(String string, DataDictionary dd, boolean validate) throws InvalidMessage { + public Message(String string, DataDictionary dd, ValidationSettings dds, boolean validate) throws InvalidMessage { initializeHeader(); - fromString(string, dd, validate, true); + fromString(string, dd, dds, validate, true); } - - public Message(String string, DataDictionary sessionDictionary, DataDictionary applicationDictionary, boolean validate) throws InvalidMessage { + + public Message(String string, DataDictionary sessionDictionary, DataDictionary applicationDictionary, ValidationSettings validationSettings) throws InvalidMessage { initializeHeader(); - fromString(string, sessionDictionary, applicationDictionary, validate, true); + fromString(string, sessionDictionary, applicationDictionary, validationSettings, true); + } + + public Message(String string, DataDictionary sessionDictionary, DataDictionary applicationDictionary, ValidationSettings validationSettings, boolean validate) throws InvalidMessage { + initializeHeader(); + fromString(string, sessionDictionary, applicationDictionary, validationSettings, validate, true, WeakParsingMode.DISABLED); } private void initializeHeader() { @@ -138,11 +163,56 @@ public Object clone() { private Object cloneTo(Message message) { message.initializeFrom(this); + message.messageData = this.messageData; message.header.initializeFrom(getHeader()); message.trailer.initializeFrom(getTrailer()); + message.remainingHeaderFields.addAll(remainingHeaderFields); + message.remainingBodyFields.addAll(remainingBodyFields); return message; } + public void replaceWith(Message message) { + clear(); + messageData = message.messageData; + initializeFrom(message); + header.initializeFrom(message.header); + trailer.initializeFrom(message.trailer); + remainingHeaderFields.addAll(message.remainingHeaderFields); + remainingBodyFields.addAll(message.remainingBodyFields); + } + + public List> getRemainingBodyFields() { + return remainingBodyFields; + } + + public Field getRemainingBodyField(int index) { + return remainingBodyFields.get(index); + } + + public boolean hasRemainingBodyFields() { + return !remainingBodyFields.isEmpty(); + } + + public Field getFirstRemainingBodyFieldByTag(int tag) { + return remainingBodyFields.stream().filter(f -> f.getTag() == tag).findFirst().orElse(null); + } + + public List> getRemainingHeaderFields() { + return remainingHeaderFields; + } + + public Field getRemainingHeaderField(int index) { + return remainingHeaderFields.get(index); + } + + public boolean hasRemainingHeaderFields() { + return !remainingHeaderFields.isEmpty(); + } + + public Field getFirstRemainingHeaderFieldByTag(int tag) { + return remainingHeaderFields.stream().filter(f -> f.getTag() == tag).findFirst().orElse(null); + } + private static final class Context { private final BodyLength bodyLength = new BodyLength(100); private final CheckSum checkSum = new CheckSum("000"); @@ -160,27 +230,30 @@ protected Context initialValue() { * Do not call this method concurrently while modifying the contents of the message. * This is likely to produce unexpected results or will fail with a ConcurrentModificationException * since FieldMap.calculateString() is iterating over the TreeMap of fields. - * + * * Use toRawString() to get the raw message data. - * + * * @return Message as String with calculated body length and checksum. */ @Override public String toString() { Context context = stringContexts.get(); - if (CharsetSupport.isStringEquivalent()) { // length & checksum can easily be calculated after message is built + if (CharsetSupport.isStringEquivalent() && lengthAndChecksumShouldBeAutomaticallyUpdated) { + // length & checksum can easily be calculated after message is built header.setField(context.bodyLength); trailer.setField(context.checkSum); - } else { + } else if (lengthAndChecksumShouldBeAutomaticallyUpdated) { header.setInt(BodyLength.FIELD, bodyLength()); trailer.setString(CheckSum.FIELD, checksum()); } StringBuilder stringBuilder = context.stringBuilder; try { header.calculateString(stringBuilder, null, null); + calculateStringRemainingFields(stringBuilder, remainingHeaderFields); calculateString(stringBuilder, null, null); + calculateStringRemainingFields(stringBuilder, remainingBodyFields); trailer.calculateString(stringBuilder, null, null); - if (CharsetSupport.isStringEquivalent()) { + if (CharsetSupport.isStringEquivalent() && lengthAndChecksumShouldBeAutomaticallyUpdated) { setBodyLength(stringBuilder); setChecksum(stringBuilder); } @@ -190,6 +263,13 @@ public String toString() { } } + protected void calculateStringRemainingFields(StringBuilder buffer, List> fields) { + for (final Field field : fields) { + field.toString(buffer); + buffer.append('\001'); + } + } + private static final String SOH = String.valueOf('\001'); private static final String BODY_LENGTH_FIELD = SOH + String.valueOf(BodyLength.FIELD) + '='; private static final String CHECKSUM_FIELD = SOH + String.valueOf(CheckSum.FIELD) + '='; @@ -215,15 +295,16 @@ private static void setChecksum(StringBuilder stringBuilder) { /** * Return the raw message data as it was passed to the Message class. - * - * This is only available after Message has been parsed via constructor or Message.fromString(). + * + * This is only available after Message has been parsed via constructor, Message.fromString() or cloned/replaced by one that was. * Otherwise this method will return NULL. - * + * * This method neither does change fields nor calculate body length or checksum. * Use toString() for that purpose. - * + * * @return Message as String without recalculating body length and checksum. */ + @Override public String toRawString() { return messageData; } @@ -446,6 +527,41 @@ public boolean isAdmin() { return false; } + @Override + public String getHeaderString(int field) throws FieldNotFound { + return header.getString(field); + } + + @Override + public void removeHeaderField(int field) { + header.removeField(field); + } + + @Override + public int getHeaderInt(int field) throws FieldNotFound { + return header.getInt(field); + } + + @Override + public void setHeaderString(int field, String value) { + header.setString(field, value); + } + + @Override + public void setHeaderInt(int field, int value) { + header.setInt(field, value); + } + + @Override + public void setHeaderUtcTimeStamp(int field, LocalDateTime localDateTime, UtcTimestampPrecision timestampPrecision) { + header.setUtcTimeStamp(field, localDateTime, timestampPrecision); + } + + @Override + public boolean isSetHeaderField(int field) { + return header.isSetField(field); + } + public boolean isApp() { return !isAdmin(); } @@ -460,6 +576,8 @@ public void clear() { super.clear(); header.clear(); trailer.clear(); + remainingHeaderFields.clear(); + remainingBodyFields.clear(); position = 0; } @@ -573,47 +691,84 @@ private void optionallySetID(Header header, int field, String value) { } } - public void fromString(String messageData, DataDictionary dd, boolean doValidation) + public void fromString(String messageData, DataDictionary dd, ValidationSettings validationSettings, boolean doValidation) throws InvalidMessage { - parse(messageData, dd, dd, doValidation, true); + parse(messageData, dd, dd, validationSettings, doValidation, true, WeakParsingMode.DISABLED); } - public void fromString(String messageData, DataDictionary dd, boolean doValidation, - boolean validateChecksum) throws InvalidMessage { - parse(messageData, dd, dd, doValidation, validateChecksum); + public void fromString(String messageData, DataDictionary dd, ValidationSettings dds, boolean doValidation, + boolean validateChecksum) throws InvalidMessage { + parse(messageData, dd, dd, dds, doValidation, validateChecksum, WeakParsingMode.DISABLED); } public void fromString(String messageData, DataDictionary sessionDictionary, - DataDictionary applicationDictionary, boolean doValidation) throws InvalidMessage { - fromString(messageData, sessionDictionary, applicationDictionary, doValidation, true); + DataDictionary applicationDictionary, ValidationSettings validationSettings, boolean doValidation) throws InvalidMessage { + fromString(messageData, sessionDictionary, applicationDictionary, validationSettings, doValidation, true, WeakParsingMode.DISABLED); } public void fromString(String messageData, DataDictionary sessionDictionary, - DataDictionary applicationDictionary, boolean doValidation, boolean validateChecksum) + DataDictionary applicationDictionary, ValidationSettings validationSettings, boolean doValidation, boolean validateChecksum, WeakParsingMode weakParsingMode) throws InvalidMessage { - if (sessionDictionary.isAdminMessage(MessageUtils.getMessageType(messageData))) { + if (sessionDictionary != null && sessionDictionary.isAdminMessage(MessageUtils.getMessageType(messageData))) { applicationDictionary = sessionDictionary; } - parse(messageData, sessionDictionary, applicationDictionary, doValidation, validateChecksum); + parse(messageData, sessionDictionary, applicationDictionary, validationSettings, doValidation, validateChecksum, weakParsingMode); } + public enum WeakParsingMode { ENABLED, FALLBACK, DISABLED } + void parse(String messageData, DataDictionary sessionDataDictionary, - DataDictionary applicationDataDictionary, boolean doValidation, - boolean validateChecksum) throws InvalidMessage { + DataDictionary applicationDataDictionary, ValidationSettings validationSettings, boolean doValidation, + boolean validateChecksum, WeakParsingMode weakParsingMode) throws InvalidMessage { this.messageData = messageData; - try { - parseHeader(sessionDataDictionary, doValidation); - parseBody(sessionDataDictionary, applicationDataDictionary, doValidation); - parseTrailer(sessionDataDictionary); - if (doValidation && validateChecksum) { - validateCheckSum(messageData); + switch (weakParsingMode) { + case ENABLED: + performWeakParse(sessionDataDictionary, applicationDataDictionary, validationSettings, doValidation, validateChecksum); + break; + case FALLBACK: + try { + performStrongParse(sessionDataDictionary, applicationDataDictionary, validationSettings, doValidation, validateChecksum); + } catch (final FieldException strongParseError) { + clear(); + performWeakParse(sessionDataDictionary, applicationDataDictionary, validationSettings, doValidation, validateChecksum); + } + break; + case DISABLED: + performStrongParse(sessionDataDictionary, applicationDataDictionary, validationSettings, doValidation, validateChecksum); + break; } } catch (final FieldException e) { exception = e; } } + private void performStrongParse(DataDictionary sessionDataDictionary, + DataDictionary applicationDataDictionary, + ValidationSettings validationSettings, + boolean doValidation, + boolean validateChecksum) throws InvalidMessage { + parseHeader(sessionDataDictionary, validationSettings, doValidation); + parseBody(sessionDataDictionary, applicationDataDictionary, validationSettings, doValidation); + parseTrailer(sessionDataDictionary); + if (doValidation && validateChecksum) { + validateCheckSum(messageData); + } + } + + private void performWeakParse(DataDictionary sessionDataDictionary, + DataDictionary applicationDataDictionary, + ValidationSettings validationSettings, + boolean doValidation, + boolean validateChecksum) throws InvalidMessage { + weakParseHeader(sessionDataDictionary, doValidation); + weakParseBody(applicationDataDictionary, validationSettings, doValidation); + weakParseTrailer(sessionDataDictionary); + if (doValidation && validateChecksum) { + validateCheckSum(messageData); + } + } + private void validateCheckSum(String messageData) throws InvalidMessage { try { // Body length is checked at the protocol layer @@ -628,7 +783,7 @@ private void validateCheckSum(String messageData) throws InvalidMessage { } } - private void parseHeader(DataDictionary dd, boolean doValidation) throws InvalidMessage { + private void parseHeader(DataDictionary dd, ValidationSettings dds, boolean doValidation) throws InvalidMessage { if (doValidation) { final boolean validHeaderFieldOrder = isNextField(dd, header, BeginString.FIELD) && isNextField(dd, header, BodyLength.FIELD) @@ -645,7 +800,38 @@ && isNextField(dd, header, BodyLength.FIELD) header.setField(field); if (dd != null && dd.isGroup(DataDictionary.HEADER_ID, field.getField())) { - parseGroup(DataDictionary.HEADER_ID, field, dd, dd, header, doValidation); + parseGroup(DataDictionary.HEADER_ID, field, dd, dd, dds, header, doValidation); + } + + field = extractField(dd, header); + } + pushBack(field); + } + + private void weakParseHeader(DataDictionary dd, boolean doValidation) throws InvalidMessage { + Set preSetFields = header.fields.keySet(); + if (doValidation) { + final boolean validHeaderFieldOrder = isNextField(dd, header, BeginString.FIELD) + && isNextField(dd, header, BodyLength.FIELD) + && isNextField(dd, header, MsgType.FIELD); + if (!validHeaderFieldOrder) { + // Invalid message preamble (first three fields) is a serious + // condition and is handled differently from other message parsing errors. + throw MessageUtils.newInvalidMessageException("Header fields out of order in " + messageData, + MessageUtils.getMinimalMessage(messageData)); + } + } + + StringField field = extractField(dd, header); + while (field != null && isHeaderField(field, dd)) { + if (remainingHeaderFields.isEmpty()) { + if (header.isSetField(field) && !preSetFields.contains(field.getField())) { + remainingHeaderFields.add(field); + } else { + header.setField(field); + } + } else { + remainingHeaderFields.add(field); } field = extractField(dd, header); @@ -670,37 +856,102 @@ private String getMsgType() throws InvalidMessage { } } - private void parseBody(DataDictionary sessionDataDictionary, DataDictionary applicationDataDictionary, boolean doValidation) throws InvalidMessage { - StringField field = extractField(applicationDataDictionary, this); + + private void parseBody(DataDictionary sdd, DataDictionary dd, ValidationSettings dds, boolean doValidation) throws InvalidMessage { + StringField field = extractField(dd, this); while (field != null) { if (isTrailerField(field.getField())) { pushBack(field); return; } - if (isHeaderField(field, sessionDataDictionary)) { + if (isHeaderField(field, sdd)) { // An acceptance test requires the sequence number to // be available even if the related field is out of order setField(header, field); // Group case - if (sessionDataDictionary != null && sessionDataDictionary.isGroup(DataDictionary.HEADER_ID, field.getField())) { - parseGroup(DataDictionary.HEADER_ID, field, sessionDataDictionary, sessionDataDictionary, header, doValidation); + if (sdd != null && sdd.isGroup(DataDictionary.HEADER_ID, field.getField())) { + parseGroup(DataDictionary.HEADER_ID, field, sdd, sdd, dds, header, doValidation); } - if (doValidation && sessionDataDictionary != null && sessionDataDictionary.isCheckFieldsOutOfOrder()) + if (doValidation && dd != null && dds.isCheckFieldsOutOfOrder()) throw new FieldException(SessionRejectReason.TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER, field.getTag()); } else { setField(this, field); // Group case - if (applicationDataDictionary != null && applicationDataDictionary.isGroup(getMsgType(), field.getField())) { - parseGroup(getMsgType(), field, applicationDataDictionary, applicationDataDictionary, this, doValidation); + if (dd != null && dd.isGroup(getMsgType(), field.getField())) { + parseGroup(getMsgType(), field, dd, dd, dds, this, doValidation); + } + } + + field = extractField(dd, this); + } + } + + private void weakParseBody(DataDictionary dd, ValidationSettings validationSettings, boolean doValidation) throws InvalidMessage { + StringField field = extractField(dd, this); + while (field != null) { + if (isTrailerField(field.getField())) { + pushBack(field); + return; + } + + if (isHeaderField(field.getField())) { + remainingHeaderFields.add(field); + if (doValidation && validationSettings != null && validationSettings.isCheckFieldsOutOfOrder()) + throw new FieldException(SessionRejectReason.TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER, + field.getTag()); + } else { + if (remainingBodyFields.isEmpty()) { + if (isSetField(field)) { + remainingBodyFields.add(field); + } else { + setField(field); + } + } else { + remainingBodyFields.add(field); } } + field = extractField(dd, this); + } + } - field = extractField(applicationDataDictionary, this); + /** + * Remove the tag before the repeated tag (should be the group count) and all subsequent tags + * @return the removed tags + */ + private List> removeAfterFirstIndex(FieldMap fieldMap, int repeatedField) { + List> removedFields = new ArrayList<>(); + Iterator> possibleCountFieldSearchIterator = fieldMap.iterator(); + Iterator> possibleRepeatedTagSearchIterator = fieldMap.iterator(); + if (!possibleRepeatedTagSearchIterator.hasNext()) { + return removedFields; + } + Field firstField = possibleRepeatedTagSearchIterator.next(); + if (firstField.getField() == repeatedField) { + removedFields.add(firstField); + return removedFields; } + Field possibleCountField = possibleCountFieldSearchIterator.next(); + while (possibleRepeatedTagSearchIterator.hasNext()) { + Field possibleRepeatedField = possibleRepeatedTagSearchIterator.next(); + if (possibleRepeatedField.getField() == repeatedField) { + removedFields.add(possibleCountField); + possibleCountFieldSearchIterator.remove(); + while (possibleCountFieldSearchIterator.hasNext()) { + Field fieldToRemove = possibleCountFieldSearchIterator.next(); + possibleCountFieldSearchIterator.remove(); + removedFields.add(fieldToRemove); + } + return removedFields; + } else { + possibleCountField = possibleCountFieldSearchIterator.next(); + } + } + return removedFields; } + private void setField(FieldMap fields, StringField field) { if (fields.isSetField(field)) { throw new FieldException(SessionRejectReason.TAG_APPEARS_MORE_THAN_ONCE, field.getTag()); @@ -708,7 +959,7 @@ private void setField(FieldMap fields, StringField field) { fields.setField(field); } - private void parseGroup(String msgType, StringField field, DataDictionary dd, DataDictionary parentDD, FieldMap parent, boolean doValidation) + private void parseGroup(String msgType, StringField field, DataDictionary dd, DataDictionary parentDD, ValidationSettings dds, FieldMap parent, boolean doValidation) throws InvalidMessage { final DataDictionary.GroupInfo rg = dd.getGroup(msgType, field.getField()); final DataDictionary groupDataDictionary = rg.getDataDictionary(); @@ -723,35 +974,39 @@ private void parseGroup(String msgType, StringField field, DataDictionary dd, Da throw MessageUtils.newInvalidMessageException("Repeating group count requires an Integer but found '" + field.getValue() + "' in " + messageData, this); } parent.setField(groupCountTag, field); - final int firstField = rg.getDelimiterField(); + int firstField = rg.getDelimiterField(); + boolean firstFieldFound = false; Group group = null; boolean inGroupParse = true; - while (inGroupParse) { - field = extractField(dd, group != null ? group : parent); - if (field == null) { - // QFJ-760: stop parsing since current position is greater than message length - break; - } - int tag = field.getTag(); - if (tag == firstField) { - addGroupRefToParent(group, parent); - group = new Group(groupCountTag, firstField, groupDataDictionary.getOrderedFields()); - group.setField(field); - previousOffset = -1; - // QFJ-742 - if (groupDataDictionary.isGroup(msgType, tag)) { - parseGroup(msgType, field, groupDataDictionary, parentDD, group, doValidation); + + if (declaredGroupCount != 0) { + while (inGroupParse) { + field = extractField(dd, group != null ? group : parent); + if (field == null) { + // QFJ-760: stop parsing since current position is greater than message length + break; } - } else if (groupDataDictionary.isGroup(msgType, tag)) { - if (group != null) { - parseGroup(msgType, field, groupDataDictionary, parentDD, group, doValidation); - } else { - // QFJ-934: message should be rejected and not ignored when first field not found - throw newFieldExceptionMissingDelimiter(groupCountTag, firstField, tag); + int tag = field.getTag(); + if (dds != null && dds.isUseFirstTagAsGroupDelimiter() && !firstFieldFound) { + firstField = tag; } - } else if (groupDataDictionary.isField(tag)) { - if (group != null) { - if (fieldOrder != null && dd.isCheckUnorderedGroupFields()) { + if (tag == firstField) { + addGroupRefToParent(group, parent); + group = new Group(groupCountTag, firstField, groupDataDictionary.getOrderedFields()); + group.setField(field); + firstFieldFound = true; + previousOffset = -1; + // QFJ-742 + if (groupDataDictionary.isGroup(msgType, tag)) { + parseGroup(msgType, field, groupDataDictionary, parentDD, dds, group, doValidation); + } + } else if (groupDataDictionary.isGroup(msgType, tag)) { + // QFJ-934: message should be rejected and not ignored when first field not found + checkFirstFieldFound(firstFieldFound, groupCountTag, firstField, tag); + parseGroup(msgType, field, groupDataDictionary, parentDD, dds, group, doValidation); + } else if (groupDataDictionary.isField(tag)) { + checkFirstFieldFound(firstFieldFound, groupCountTag, firstField, tag); + if (fieldOrder != null && (dds == null || dds.isCheckUnorderedGroupFields())) { final int offset = indexOf(tag, fieldOrder); if (offset > -1) { if (offset <= previousOffset) { @@ -759,28 +1014,29 @@ private void parseGroup(String msgType, StringField field, DataDictionary dd, Da group.setField(field); addGroupRefToParent(group, parent); throw new FieldException( - SessionRejectReason.REPEATING_GROUP_FIELDS_OUT_OF_ORDER, tag); + SessionRejectReason.REPEATING_GROUP_FIELDS_OUT_OF_ORDER, tag); } previousOffset = offset; } } group.setField(field); + } else if (group == null) { + throw new FieldException( + SessionRejectReason.REPEATING_GROUP_FIELDS_OUT_OF_ORDER, "Missing first tag in repeating group " + groupCountTag + ". Expected " + firstField + " to be the first tag in the group", tag); } else { - throw newFieldExceptionMissingDelimiter(groupCountTag, firstField, tag); - } - } else { - // QFJ-169/QFJ-791: handle unknown repeating group fields in the body - if (!isTrailerField(tag) && !(DataDictionary.HEADER_ID.equals(msgType) || isHeaderField(field, dd))) { - if (checkFieldValidation(parent, parentDD, field, msgType, doValidation, group)) { - continue; + // QFJ-169/QFJ-791: handle unknown repeating group fields in the body + if (!isTrailerField(tag) && !isHeaderField(field.getField()) && !(DataDictionary.HEADER_ID.equals(msgType))) { + if (checkFieldValidation(parent, parentDD, dds, field, msgType, doValidation, group, parent.getGroups(group.getFieldTag()), declaredGroupCount)) { + continue; + } } + pushBack(field); + inGroupParse = false; } - pushBack(field); - inGroupParse = false; } + // add what we've already got and leave the rest to the validation (if enabled) + addGroupRefToParent(group, parent); } - // add what we've already got and leave the rest to the validation (if enabled) - addGroupRefToParent(group, parent); // For later validation that the group size matches the parsed group count parent.setGroupCount(groupCountTag, declaredGroupCount); } @@ -791,17 +1047,19 @@ private void addGroupRefToParent(Group group, FieldMap parent) { } } - private FieldException newFieldExceptionMissingDelimiter(final int groupCountTag, final int firstField, int tag) throws FieldException { - return new FieldException( + private void checkFirstFieldFound(boolean firstFieldFound, final int groupCountTag, final int firstField, int tag) throws FieldException { + if (!firstFieldFound) { + throw new FieldException( SessionRejectReason.REPEATING_GROUP_FIELDS_OUT_OF_ORDER, "The group " + groupCountTag + " must set the delimiter field " + firstField, tag); + } } - private boolean checkFieldValidation(FieldMap parent, DataDictionary parentDD, StringField field, String msgType, boolean doValidation, Group group) throws FieldException { + private boolean checkFieldValidation(FieldMap parent, DataDictionary parentDD, ValidationSettings dds, StringField field, String msgType, boolean doValidation, Group group, List previousGroups, int groupCount) throws FieldException { boolean isField = (parent instanceof Group) ? parentDD.isField(field.getTag()) : parentDD.isMsgField(msgType, field.getTag()); if (!isField) { if (doValidation) { - boolean fail = parentDD.checkFieldFailure(field.getTag(), false); + boolean fail = parentDD.checkFieldFailure(dds, field.getTag(), false); if (fail) { throw new FieldException(SessionRejectReason.TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE, field.getTag()); } @@ -810,8 +1068,32 @@ private boolean checkFieldValidation(FieldMap parent, DataDictionary parentDD, S throw new FieldException( SessionRejectReason.REPEATING_GROUP_FIELDS_OUT_OF_ORDER, field.getTag()); } - group.setField(field); - return true; + boolean lastGroup = previousGroups.size() + 1 >= groupCount; + if (!lastGroup) { + group.setField(field); + return true; + } else { + if (dds.isOnlyAllowSeenOrKnownFieldsInLastGroup()) { + if (isSeenField(field, previousGroups)) { + group.setField(field); + return true; + } else { + return false; + } + } else { + group.setField(field); + return true; + } + } + } + return false; + } + + private boolean isSeenField(Field field, List groups) { + for (Group g : groups) { + if (g.isSetField(field)) { + return true; + } } return false; } @@ -828,6 +1110,18 @@ private void parseTrailer(DataDictionary dd) throws InvalidMessage { } } + private void weakParseTrailer(DataDictionary dd) throws InvalidMessage { + StringField field = extractField(dd, trailer); + while (field != null) { + if (!isTrailerField(field, dd)) { + throw new FieldException(SessionRejectReason.TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER, + field.getTag()); + } + trailer.setField(field); + field = extractField(dd, trailer); + } + } + static boolean isHeaderField(Field field, DataDictionary dd) { return isHeaderField(field.getField()) || (dd != null && dd.isHeaderField(field.getField())); @@ -969,6 +1263,7 @@ boolean hasValidStructure() { return exception == null; } + @Override public FieldException getException() { return exception; } @@ -1001,7 +1296,7 @@ public static MsgType identifyType(String message) throws MessageParseError { boolean isGarbled() { return isGarbled; } - + void setGarbled(boolean isGarbled) { this.isGarbled = isGarbled; } diff --git a/quickfixj-core/src/main/java/quickfix/MessageQueue.java b/quickfixj-core/src/main/java/quickfix/MessageQueue.java index 79e4e16ee8..8f48cc7624 100644 --- a/quickfixj-core/src/main/java/quickfix/MessageQueue.java +++ b/quickfixj-core/src/main/java/quickfix/MessageQueue.java @@ -24,7 +24,7 @@ * * @see quickfix.Session */ -interface MessageQueue { +public interface MessageQueue { /** * Enqueue a message. diff --git a/quickfixj-core/src/main/java/quickfix/MessageStore.java b/quickfixj-core/src/main/java/quickfix/MessageStore.java index e508649141..756c72dad0 100644 --- a/quickfixj-core/src/main/java/quickfix/MessageStore.java +++ b/quickfixj-core/src/main/java/quickfix/MessageStore.java @@ -19,6 +19,7 @@ package quickfix; +import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.io.IOException; @@ -73,6 +74,14 @@ public interface MessageStore { */ Date getCreationTime() throws IOException; + /** + * Get the session creation time as a calendar object. + * + * @return the session creation time. + * @throws IOException IO error + */ + Calendar getCreationTimeCalendar() throws IOException; + /** * Reset the message store. Sequence numbers are set back to 1 and stored * messages are erased. The session creation time is also set to the time of diff --git a/quickfixj-core/src/main/java/quickfix/MessageUtils.java b/quickfixj-core/src/main/java/quickfix/MessageUtils.java index 1fa0a67a19..115a04c422 100644 --- a/quickfixj-core/src/main/java/quickfix/MessageUtils.java +++ b/quickfixj-core/src/main/java/quickfix/MessageUtils.java @@ -35,6 +35,7 @@ import java.nio.charset.Charset; import java.util.HashMap; +import java.util.List; import java.util.Map; public class MessageUtils { @@ -90,9 +91,9 @@ private static String getFieldOrDefault(FieldMap fields, int tag, String default } } - public static Message parse(MessageFactory messageFactory, DataDictionary dataDictionary, + public static Message parse(MessageFactory messageFactory, DataDictionary dataDictionary, ValidationSettings validationSettings, String messageString) throws InvalidMessage { - return parse(messageFactory, dataDictionary, messageString, true); + return parse(messageFactory, dataDictionary, validationSettings, messageString, true); } /** @@ -105,7 +106,7 @@ public static Message parse(MessageFactory messageFactory, DataDictionary dataDi * @return the parsed message * @throws InvalidMessage */ - public static Message parse(MessageFactory messageFactory, DataDictionary dataDictionary, + public static Message parse(MessageFactory messageFactory, DataDictionary dataDictionary, ValidationSettings validationSettings, String messageString, boolean validateChecksum) throws InvalidMessage { final int index = messageString.indexOf(FIELD_SEPARATOR); if (index < 0) { @@ -114,7 +115,7 @@ public static Message parse(MessageFactory messageFactory, DataDictionary dataDi final String beginString = messageString.substring(2, index); final String messageType = getMessageType(messageString); final quickfix.Message message = messageFactory.create(beginString, messageType); - message.fromString(messageString, dataDictionary, dataDictionary != null, validateChecksum); + message.fromString(messageString, dataDictionary, validationSettings, dataDictionary != null, validateChecksum); return message; } @@ -122,15 +123,17 @@ public static Message parse(MessageFactory messageFactory, DataDictionary dataDi * NOTE: This method is intended for internal use. * * @param session the Session that will process the message - * @param messageString + * @param messageString the message in raw string format * @return the parsed message - * @throws InvalidMessage + * @throws InvalidMessage when the ApplVerID is missing (for FIXT messages) or the message can't be parsed */ public static Message parse(Session session, String messageString) throws InvalidMessage { final String beginString = getStringField(messageString, BeginString.FIELD); final String msgType = getMessageType(messageString); final MessageFactory messageFactory = session.getMessageFactory(); - final DataDictionaryProvider ddProvider = session.getDataDictionaryProvider(); + final DataDictionaryProvider ddProvider = shouldUseDictionary(session, msgType) ? + session.getDataDictionaryProvider() : null; + final ValidationSettings validationSettings = session.getValidationSettings(); final ApplVerID applVerID; final DataDictionary sessionDataDictionary = ddProvider == null ? null : ddProvider .getSessionDataDictionary(beginString); @@ -157,12 +160,26 @@ public static Message parse(Session session, String messageString) throws Invali final boolean validateChecksum = session.isValidateChecksum(); message = messageFactory.create(beginString, applVerID, msgType); - message.parse(messageString, sessionDataDictionary, payloadDictionary, doValidation, - validateChecksum); + message.parse(messageString, sessionDataDictionary, payloadDictionary, validationSettings, doValidation, + validateChecksum, session.getWeakParsingMode()); + + if (payloadDictionary != null && session.getUseDictionaryOrdering()) { + message.setFieldOrder(payloadDictionary.getOrderedFieldsForMessage(msgType)); + for (List groups : message.getGroups().values()) { + for (Group group : groups) { + group.setFieldOrder(payloadDictionary.getGroup(msgType, + group.getFieldTag()).getDataDictionary().getOrderedFields()); + } + } + } return message; } + private static boolean shouldUseDictionary(Session session, String msgType) { + return session.useDictionaryForMsgType(msgType); + } + private static ApplVerID getApplVerID(Session session, String messageString) throws InvalidMessage { ApplVerID applVerID = null; @@ -393,7 +410,7 @@ public static int checksum(String message) { public static int length(Charset charset, String data) { return CharsetSupport.isStringEquivalent(charset) ? data.length() : data.getBytes(charset).length; } - + /** * Returns an InvalidMessage Exception with optionally attached FIX message. * diff --git a/quickfixj-core/src/main/java/quickfix/NoopStore.java b/quickfixj-core/src/main/java/quickfix/NoopStore.java index ddc33b54ca..8bdc4cc354 100644 --- a/quickfixj-core/src/main/java/quickfix/NoopStore.java +++ b/quickfixj-core/src/main/java/quickfix/NoopStore.java @@ -20,6 +20,7 @@ package quickfix; +import java.util.Calendar; import java.util.Collection; import java.util.Date; @@ -31,6 +32,7 @@ public class NoopStore implements MessageStore { private Date creationTime = new Date(); + private Calendar creationTimeCalendar = SystemTime.getUtcCalendar(creationTime); private int nextSenderMsgSeqNum = 1; private int nextTargetMsgSeqNum = 1; @@ -41,6 +43,10 @@ public Date getCreationTime() { return creationTime; } + public Calendar getCreationTimeCalendar() { + return creationTimeCalendar; + } + public int getNextSenderMsgSeqNum() { return nextSenderMsgSeqNum; } diff --git a/quickfixj-core/src/main/java/quickfix/SLF4JLog.java b/quickfixj-core/src/main/java/quickfix/SLF4JLog.java index 33fa52cd90..c7a5ab9d52 100644 --- a/quickfixj-core/src/main/java/quickfix/SLF4JLog.java +++ b/quickfixj-core/src/main/java/quickfix/SLF4JLog.java @@ -111,11 +111,13 @@ private String substituteVariables(SessionID sessionID, String category) { return processedCategory; } + @Override public void onEvent(String text) { log(eventLog, text); } - public void onErrorEvent(String text) { + @Override + public void onErrorEvent(String category, String text) { logError(errorEventLog, text); } @@ -149,6 +151,7 @@ protected void logError(org.slf4j.Logger log, String text) { log.error(message); } + @Override public void clear() { onEvent("Log clear operation is not supported: " + getClass().getName()); } diff --git a/quickfixj-core/src/main/java/quickfix/ScreenLog.java b/quickfixj-core/src/main/java/quickfix/ScreenLog.java index c3ee23331a..2f982ddbc3 100644 --- a/quickfixj-core/src/main/java/quickfix/ScreenLog.java +++ b/quickfixj-core/src/main/java/quickfix/ScreenLog.java @@ -62,12 +62,14 @@ public class ScreenLog extends AbstractLog { this.includeMillis = includeMillis; } + @Override protected void logIncoming(String message) { if (incoming) { logMessage(message, INCOMING_CATEGORY); } } + @Override protected void logOutgoing(String message) { if (outgoing) { logMessage(message, OUTGOING_CATEGORY); @@ -78,13 +80,15 @@ private void logMessage(String message, String type) { log(message, type); } + @Override public void onEvent(String message) { if (events) { log(message, EVENT_CATEGORY); } } - public void onErrorEvent(String message) { + @Override + public void onErrorEvent(String category, String message) { log(message, ERROR_EVENT_CATEGORY); } @@ -98,6 +102,7 @@ void setOut(PrintStream out) { this.out = out; } + @Override public void clear() { onEvent("Log clear operation is not supported: " + getClass().getName()); } diff --git a/quickfixj-core/src/main/java/quickfix/SendResult.java b/quickfixj-core/src/main/java/quickfix/SendResult.java new file mode 100644 index 0000000000..6acdc0e451 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/SendResult.java @@ -0,0 +1,31 @@ +package quickfix; + +public enum SendResult { + DO_NOT_SEND(false, false, false), + NOT_PERSISTED_NOT_SENT(false, false, false), + PERSISTED_NOT_SENT(false, true, false), + NOT_PERSISTED_SENT(true, false, true), + PERSISTED_SENT(true, true, true); + + private boolean originalResult; + private boolean persisted; + private boolean sent; + + SendResult(boolean originalResult, boolean persisted, boolean sent) { + this.originalResult = originalResult; + this.persisted = persisted; + this.sent = sent; + } + + public boolean getOriginalResult() { + return originalResult; + } + + public boolean isPersisted() { + return persisted; + } + + public boolean isSent() { + return sent; + } +} diff --git a/quickfixj-core/src/main/java/quickfix/Session.java b/quickfixj-core/src/main/java/quickfix/Session.java index 7e52be07bf..b24c78be1b 100644 --- a/quickfixj-core/src/main/java/quickfix/Session.java +++ b/quickfixj-core/src/main/java/quickfix/Session.java @@ -61,10 +61,7 @@ import java.net.InetAddress; import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; @@ -157,6 +154,11 @@ public class Session implements Closeable { */ public static final String SETTING_WEEKDAYS = "Weekdays"; + /** + * Session scheduling setting to specify active days of the week. + */ + public static final String SETTING_TIME_PERIODS = "TimePeriods"; + /** * Session setting to indicate whether a data dictionary should be used. If * a data dictionary is not used then message validation is not possible. @@ -322,6 +324,16 @@ public class Session implements Closeable { */ public static final String SETTING_ALLOW_UNKNOWN_MSG_FIELDS = "AllowUnknownMsgFields"; + /** + * Use the first tag in the group as the delimiter rather than rely on dictionary definition + */ + public static final String SETTING_USE_FIRST_TAG_AS_GROUP_DELIMITER = "UseFirstTagAsGroupDelimiter"; + + /** + * In the last group, consider unknown fields out of the group unless they've been seen in a previous group + */ + public static final String SETTING_ONLY_ALLOW_SEEN_OR_KNOWN_FIELDS_IN_LAST_GROUP = "OnlyAllowSeenOrKnownFieldsInLastGroup"; + public static final String SETTING_DEFAULT_APPL_VER_ID = "DefaultApplVerID"; /** @@ -379,11 +391,30 @@ public class Session implements Closeable { public static final String SETTING_MAX_SCHEDULED_WRITE_REQUESTS = "MaxScheduledWriteRequests"; public static final String SETTING_VALIDATE_CHECKSUM = "ValidateChecksum"; + public static final String SETTING_WEAK_PARSING_MODE = "WeakParsingMode"; /** * Option so that the session does not remove PossDupFlag (43) and OrigSendingTime (122) information when sending. */ public static final String SETTING_ALLOW_POS_DUP_MESSAGES = "AllowPosDup"; + /** + * Added by Flextrade + * Setting telling us to treat FIX.4.1 resend requests as FIX.4.2 ones. I.e. requesting to '0' actually + * requests to infinity. + */ + public static final String SETTING_FIX_41_RESEND_AS_FIX42 = "Fix41ResendRequestAsFix42"; + + /** + * Added by FlexTrade + * Allows overriding of the default 'Fields,Groups' ordering with the order in the incoming DD + */ + public static final String SETTING_USE_DICTIONARY_ORDERING = "UseDictionaryOrdering"; + + public static final String SETTING_MAX_MESSAGES_QUEUED_WHILE_PENDING_RESEND = "MaxMessagesQueuedWhilePendingResend"; + + public static final String SETTING_MAX_RESEND_RETRIEVAL_BATCH_SIZE = "MaxResendBatchRetrievalSize"; + public static final String SETTING_USE_DATA_DICTIONARY_FOR_MSG_TYPES = "UseDataDictionaryForMsgTypes"; + public static final String SETTING_REJECT_MESSAGE_OUT_OF_TIME = "RejectMessageOutOfTime"; private static final ConcurrentMap sessions = new ConcurrentHashMap<>(); @@ -412,6 +443,7 @@ public class Session implements Closeable { private long lastSessionLogon = 0; private final DataDictionaryProvider dataDictionaryProvider; + private final ValidationSettings validationSettings; private final boolean checkLatency; private final int maxLatency; private int resendRequestChunkSize = 0; @@ -436,9 +468,19 @@ public class Session implements Closeable { private boolean enableLastMsgSeqNumProcessed = false; private boolean validateChecksum = true; private boolean allowPosDup = false; + private boolean fix41ResendRequestAsFix42 = false; + private boolean useDictionaryOrdering = false; + private boolean rejectMessageOutOfTime = true; + private Message.WeakParsingMode weakParsingMode = Message.WeakParsingMode.DISABLED; + + private int maxResendBatchRetrievalSize = DEFAULT_MAX_RESEND_BATCH_RETRIEVAL_SIZE; private int maxScheduledWriteRequests = 0; + private SessionResendListener sessionResendListener; + + private IgnoredGarbledMessageListener ignoredGarbledMessageListener; + private final AtomicBoolean isResetting = new AtomicBoolean(); private final AtomicBoolean isResettingState = new AtomicBoolean(); @@ -459,7 +501,6 @@ public class Session implements Closeable { public static final double DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER = 1.4; private static final String ENCOUNTERED_END_OF_STREAM = "Encountered END_OF_STREAM"; - private static final int BAD_COMPID_REJ_REASON = SessionRejectReason.COMPID_PROBLEM; private static final String BAD_COMPID_TEXT = new FieldException(BAD_COMPID_REJ_REASON).getMessage(); private static final int BAD_TIME_REJ_REASON = SessionRejectReason.SENDINGTIME_ACCURACY_PROBLEM; @@ -467,21 +508,25 @@ public class Session implements Closeable { private static final String BAD_TIME_TEXT = new FieldException(BAD_TIME_REJ_REASON, SendingTime.FIELD).getMessage(); private final List logonTags; + private final Set msgTypesToUseDictionary; protected static final Logger LOG = LoggerFactory.getLogger(Session.class); + public static final int DEFAULT_MAX_RESEND_BATCH_RETRIEVAL_SIZE = 1000; + Session(Application application, MessageStoreFactory messageStoreFactory, SessionID sessionID, - DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule, LogFactory logFactory, + DataDictionaryProvider dataDictionaryProvider, ValidationSettings validationSettings, SessionSchedule sessionSchedule, LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval) { - this(application, messageStoreFactory, sessionID, dataDictionaryProvider, sessionSchedule, logFactory, + this(application, messageStoreFactory, sessionID, dataDictionaryProvider, validationSettings, sessionSchedule, logFactory, messageFactory, heartbeatInterval, true, DEFAULT_MAX_LATENCY, UtcTimestampPrecision.MILLIS, false, false, false, false, true, false, true, false, DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, null, true, new int[] {5}, false, false, false, false, true, false, true, false, null, true, DEFAULT_RESEND_RANGE_CHUNK_SIZE, false, - false, false, new ArrayList(), DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false); + false, false, new ArrayList(), DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false, false); } Session(Application application, MessageStoreFactory messageStoreFactory, SessionID sessionID, - DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule, + DataDictionaryProvider dataDictionaryProvider, ValidationSettings validationSettings, + SessionSchedule sessionSchedule, LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval, boolean checkLatency, int maxLatency, UtcTimestampPrecision timestampPrecision, boolean resetOnLogon, boolean resetOnLogout, boolean resetOnDisconnect, @@ -496,16 +541,28 @@ public class Session implements Closeable { boolean validateIncomingMessage, int resendRequestChunkSize, boolean enableNextExpectedMsgSeqNum, boolean enableLastMsgSeqNumProcessed, boolean validateChecksum, List logonTags, double heartBeatTimeoutMultiplier, - boolean allowPossDup) { - this(application, messageStoreFactory, new InMemoryMessageQueueFactory(), sessionID, dataDictionaryProvider, sessionSchedule, logFactory, - messageFactory, heartbeatInterval, true, DEFAULT_MAX_LATENCY, UtcTimestampPrecision.MILLIS, false, false, - false, false, true, false, true, false, DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, null, true, new int[] {5}, - false, false, false, false, true, false, true, false, null, true, DEFAULT_RESEND_RANGE_CHUNK_SIZE, false, - false, false, new ArrayList(), DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, allowPossDup); + boolean useDictionaryOrdering, boolean allowPossDup) { + this(application, messageStoreFactory, new InMemoryMessageQueueFactory(), sessionID, dataDictionaryProvider, + null, sessionSchedule, logFactory, + messageFactory, heartbeatInterval, true, DEFAULT_MAX_LATENCY, UtcTimestampPrecision.MILLIS, + false, false, + false, false, true, false, + true, false, DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, null, + true, new int[] {5}, + false, false, false, false, true, + false, true, false, + null, true, + DEFAULT_RESEND_RANGE_CHUNK_SIZE, false, + false, false, new ArrayList(), + DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false, allowPossDup, + Message.WeakParsingMode.DISABLED, DEFAULT_MAX_RESEND_BATCH_RETRIEVAL_SIZE, + new HashSet<>(), + true); } Session(Application application, MessageStoreFactory messageStoreFactory, MessageQueueFactory messageQueueFactory, - SessionID sessionID, DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule, + SessionID sessionID, DataDictionaryProvider dataDictionaryProvider, + ValidationSettings validationSettings, SessionSchedule sessionSchedule, LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval, boolean checkLatency, int maxLatency, UtcTimestampPrecision timestampPrecision, boolean resetOnLogon, boolean resetOnLogout, boolean resetOnDisconnect, @@ -520,7 +577,8 @@ public class Session implements Closeable { boolean validateIncomingMessage, int resendRequestChunkSize, boolean enableNextExpectedMsgSeqNum, boolean enableLastMsgSeqNumProcessed, boolean validateChecksum, List logonTags, double heartBeatTimeoutMultiplier, - boolean allowPossDup) { + boolean useDictionaryOrdering, boolean allowPossDup, Message.WeakParsingMode weakParsingMode, int maxResendBatchRetrievalSize, + Set msgTypesToUseDictionary, boolean rejectMessageOutOfTime) { this.application = application; this.sessionID = sessionID; this.sessionSchedule = sessionSchedule; @@ -532,6 +590,7 @@ public class Session implements Closeable { this.timestampPrecision = timestampPrecision; this.refreshOnLogon = refreshOnLogon; this.dataDictionaryProvider = dataDictionaryProvider; + this.validationSettings = validationSettings; this.messageFactory = messageFactory; this.checkCompID = checkCompID; this.redundantResentRequestsAllowed = redundantResentRequestsAllowed; @@ -556,6 +615,11 @@ public class Session implements Closeable { this.validateChecksum = validateChecksum; this.logonTags = logonTags; this.allowPosDup = allowPossDup; + this.useDictionaryOrdering = useDictionaryOrdering; + this.weakParsingMode = weakParsingMode; + this.maxResendBatchRetrievalSize = maxResendBatchRetrievalSize; + this.msgTypesToUseDictionary = msgTypesToUseDictionary; + this.rejectMessageOutOfTime = rejectMessageOutOfTime; final Log engineLog = (logFactory != null) ? logFactory.create(sessionID) : null; if (engineLog instanceof SessionStateListener) { @@ -576,12 +640,12 @@ public class Session implements Closeable { messageStore, messageQueue, testRequestDelayMultiplier, heartBeatTimeoutMultiplier); registerSession(this); - + getLog().onEvent("Session " + sessionID + " weak-parsing mode is " + weakParsingMode); getLog().onEvent("Session " + sessionID + " schedule is " + sessionSchedule); try { resetIfSessionNotCurrent(sessionID, SystemTime.currentTimeMillis()); } catch (final IOException e) { - LogUtil.logThrowable(getLog(), "error during session construction", e); + LogUtil.logThrowable(getLog(), ErrorEventReasons.FAILED_TO_CREATE_SESSION, "error during session construction", e); } // QFJ-721: for non-FIXT sessions we do not need to set targetDefaultApplVerID from Logon @@ -610,7 +674,7 @@ public void setResponder(Responder responder) { if (responder != null) { stateListener.onConnect(sessionID); } else { - stateListener.onDisconnect(sessionID); + stateListener.onDisconnect(sessionID, "No responder"); } } } @@ -646,7 +710,7 @@ public String getRemoteAddress() { private boolean isCurrentSession(final long time) throws IOException { return sessionSchedule == null || sessionSchedule.isSameSession( - SystemTime.getUtcCalendar(time), SystemTime.getUtcCalendar(state.getCreationTime())); + SystemTime.getUtcCalendar(time), state.getCreationTimeCalendar()); } /** @@ -657,7 +721,7 @@ private boolean isCurrentSession(final long time) * @return true is send was successful, false otherwise * @throws SessionNotFound if session could not be located */ - public static boolean sendToTarget(Message message) throws SessionNotFound { + public static SendResult sendToTarget(Message message) throws SessionNotFound { return sendToTarget(message, ""); } @@ -671,7 +735,7 @@ public static boolean sendToTarget(Message message) throws SessionNotFound { * @return true is send was successful, false otherwise * @throws SessionNotFound if session could not be located */ - public static boolean sendToTarget(Message message, String qualifier) throws SessionNotFound { + public static SendResult sendToTarget(Message message, String qualifier) throws SessionNotFound { try { final String senderCompID = getSenderCompIDFromMessage(message); final String targetCompID = getTargetCompIDFromMessage(message); @@ -700,7 +764,7 @@ private static String getSenderCompIDFromMessage(final Message message) throws F * @return true is send was successful, false otherwise * @throws SessionNotFound if session could not be located */ - public static boolean sendToTarget(Message message, String senderCompID, String targetCompID) + public static SendResult sendToTarget(Message message, String senderCompID, String targetCompID) throws SessionNotFound { return sendToTarget(message, senderCompID, targetCompID, ""); } @@ -718,7 +782,7 @@ public static boolean sendToTarget(Message message, String senderCompID, String * @return true is send was successful, false otherwise * @throws SessionNotFound if session could not be located */ - public static boolean sendToTarget(Message message, String senderCompID, String targetCompID, + public static SendResult sendToTarget(Message message, String senderCompID, String targetCompID, String qualifier) throws SessionNotFound { try { return sendToTarget(message, @@ -739,7 +803,7 @@ public static boolean sendToTarget(Message message, String senderCompID, String * @return true is send was successful, false otherwise * @throws SessionNotFound if session could not be located */ - public static boolean sendToTarget(Message message, SessionID sessionID) throws SessionNotFound { + public static SendResult sendToTarget(Message message, SessionID sessionID) throws SessionNotFound { final Session session = lookupSession(sessionID); if (session == null) { throw new SessionNotFound(); @@ -795,27 +859,27 @@ private void setEnabled(boolean enabled) { this.enabled = enabled; } - private void initializeHeader(Message.Header header) { + private void initializeHeader(IMessage message) { 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) { + message.setHeaderString(BeginString.FIELD, sessionID.getBeginString()); + message.setHeaderString(SenderCompID.FIELD, sessionID.getSenderCompID()); + optionallySetHeaderID(message, SenderSubID.FIELD, sessionID.getSenderSubID()); + optionallySetHeaderID(message, SenderLocationID.FIELD, sessionID.getSenderLocationID()); + message.setHeaderString(TargetCompID.FIELD, sessionID.getTargetCompID()); + optionallySetHeaderID(message, TargetSubID.FIELD, sessionID.getTargetSubID()); + optionallySetHeaderID(message, TargetLocationID.FIELD, sessionID.getTargetLocationID()); + message.setHeaderInt(MsgSeqNum.FIELD, getExpectedSenderNum()); + insertSendingTime(message); + } + + private void optionallySetHeaderID(IMessage message, int field, String value) { if (!value.equals(SessionID.NOT_SET)) { - header.setString(field, value); + message.setHeaderString(field, value); } } - private void insertSendingTime(Message.Header header) { - header.setUtcTimeStamp(SendingTime.FIELD, SystemTime.getLocalDateTime(), getTimestampPrecision()); + private void insertSendingTime(IMessage message) { + message.setHeaderUtcTimeStamp(SendingTime.FIELD, SystemTime.getLocalDateTime(), getTimestampPrecision()); } private UtcTimestampPrecision getTimestampPrecision() { @@ -914,8 +978,18 @@ private boolean isResetNeeded() { * Logs out session (if logged on) and then resets session state. * * @see SessionState#reset() + * @deprecated Use {@link #reset(String)} */ public void reset() { + reset("Session reset"); + } + + /** + * Logs out session (if logged on) and then resets session state. + * + * @see SessionState#reset() + */ + public void reset(String cause) { if (!isResetting.compareAndSet(false, true)) { return; } @@ -925,7 +999,7 @@ public void reset() { ((ApplicationExtended) application).onBeforeSessionReset(sessionID); } state.setResetStatePending(true); - generateLogout("Session reset"); + generateLogout(cause); getLog().onEvent("Initiated logout request"); } else { resetState(); @@ -967,7 +1041,7 @@ public int getExpectedSenderNum() { try { return state.getMessageStore().getNextSenderMsgSeqNum(); } catch (final IOException e) { - getLog().onErrorEvent("getNextSenderMsgSeqNum failed: " + e.getMessage()); + getLog().onErrorEvent(ErrorEventReasons.GET_NEXT_SEQ_NUM_FAILURE, "getNextSenderMsgSeqNum failed: " + e.getMessage()); return -1; } } @@ -982,7 +1056,7 @@ public int getExpectedTargetNum() { try { return state.getMessageStore().getNextTargetMsgSeqNum(); } catch (final IOException e) { - getLog().onErrorEvent("getNextTargetMsgSeqNum failed: " + e.getMessage()); + getLog().onErrorEvent(ErrorEventReasons.GET_NEXT_SEQ_NUM_FAILURE,"getNextTargetMsgSeqNum failed: " + e.getMessage()); return -1; } } @@ -1040,7 +1114,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi } } - if (validateIncomingMessage && dataDictionaryProvider != null) { + if (validateIncomingMessage && dataDictionaryProvider != null && useDictionaryForMsgType(msgType)) { final DataDictionary sessionDataDictionary = dataDictionaryProvider .getSessionDataDictionary(beginString); @@ -1053,38 +1127,22 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi .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 + ": " + getMessageToLog(message)); - } - } catch (final FieldException e) { - if (message.isSetField(e.getField())) { - if (rejectInvalidMessage) { - throw e; + if (!Message.WeakParsingMode.ENABLED.equals(weakParsingMode)) { + try { + DataDictionary.validate(message, sessionDataDictionary, + applicationDataDictionary, validationSettings); + } catch (final IncorrectTagValue e) { + throwOrLog(message, "Warn: incoming message with " + e, e); + } catch (final FieldException e) { + if (message.isSetField(e.getField())) { + throwOrLog(message, "Warn: incoming message with incorrect field: " + + message.getField(e.getField()), e); } else { - getLog().onErrorEvent( - "Warn: incoming message with incorrect field: " - + message.getField(e.getField()) + ": " + getMessageToLog(message)); + throwOrLog(message, "Warn: incoming message with missing field: " + e.getField() + ": " + + e.getMessage(), e); } - } else { - if (rejectInvalidMessage) { - throw e; - } else { - getLog().onErrorEvent( - "Warn: incoming message with missing field: " + e.getField() - + ": " + e.getMessage() + ": " + getMessageToLog(message)); - } - } - } catch (final FieldNotFound e) { - if (rejectInvalidMessage) { - throw e; - } else { - getLog().onErrorEvent("Warn: incoming " + e + ": " + getMessageToLog(message)); + } catch (final FieldNotFound e) { + throwOrLog(message, "Warn: incoming " + e, e); } } } @@ -1133,7 +1191,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi BusinessRejectReason.CONDITIONALLY_REQUIRED_FIELD_MISSING, e.field); } else { if (MsgType.LOGON.equals(msgType)) { - getLog().onErrorEvent("Required field missing from logon"); + getLog().onErrorEvent(ErrorEventReasons.REQUIRED_FIELD_MISSING, "Required field missing from logon"); disconnect("Required field missing from logon", true); } else { generateReject(message, SessionRejectReason.REQUIRED_TAG_MISSING, e.field); @@ -1146,18 +1204,24 @@ ignore the message and let the problem correct itself (optimistic approach). on the next message that is received. If the message should get rejected and the seqnum get incremented, then setting RejectGarbledMessage=Y needs to be used. */ + String messageToLog = getMessageToLog(message); if (rejectGarbledMessage) { - getLog().onErrorEvent("Processing garbled message: " + e.getMessage()); + getLog().onErrorEvent(ErrorEventReasons.GARBLED_MESSAGE, "Processing garbled message: " + e.getMessage()); + getLog().onInvalidMessage(messageToLog, "Skipping invalid message: " + e.getMessage()); generateReject(message, "Message failed basic validity check"); } else { - getLog().onErrorEvent("Skipping invalid message: " + e + ": " + getMessageToLog(message)); + getLog().onErrorEvent(ErrorEventReasons.SKIPPING_INVALID_MESSAGE, "Skipping invalid message: " + e + ": " + getMessageToLog(message)); + getLog().onInvalidMessage(messageToLog, "Skipping invalid message: " + e.getMessage()); + if (ignoredGarbledMessageListener != null) { + ignoredGarbledMessageListener.garbledMessageIgnored(sessionID, message); + } if (resetOrDisconnectIfRequired(message)) { return; } } } catch (final RejectLogon e) { final String rejectMessage = e.getMessage() != null ? (": " + e) : ""; - getLog().onErrorEvent("Logon rejected" + rejectMessage); + getLog().onErrorEvent(ErrorEventReasons.LOGON_REJECTED, "Logon rejected" + rejectMessage); if (e.isLogoutBeforeDisconnect()) { if (e.getSessionStatus() > -1) { generateLogout(e.getMessage(), new SessionStatus(e.getSessionStatus())); @@ -1186,7 +1250,9 @@ ignore the message and let the problem correct itself (optimistic approach). if (MsgType.LOGOUT.equals(msgType)) { nextLogout(message); } else { - generateLogout("Incorrect BeginString: " + e.getMessage()); + String failureReason = "Incorrect BeginString: " + e.getMessage(); + getLog().onInvalidMessage(getMessageToLog(message), failureReason); + generateLogout(failureReason); state.incrNextTargetMsgSeqNum(); // 1d_InvalidLogonWrongBeginString.def appears to require // a disconnect although the C++ didn't appear to be doing it. @@ -1194,16 +1260,20 @@ ignore the message and let the problem correct itself (optimistic approach). disconnect("Incorrect BeginString: " + e, true); } } catch (final IOException e) { - LogUtil.logThrowable(sessionID, "Error processing message: " + getMessageToLog(message), e); + String messageToLog = getMessageToLog(message); + LogUtil.logThrowable(sessionID, ErrorEventReasons.IO_ERROR, "Error processing message: " + messageToLog, e); + getLog().onInvalidMessage(messageToLog, "Error processing message: " + e.getMessage()); if (resetOrDisconnectIfRequired(message)) { return; } - } catch (Throwable t) { // QFJ-572 - // If there are any other Throwables we might catch them here if desired. + } catch (Exception e) { // QFJ-572 + // If there are any other Exceptions we might catch them here if desired. // They were most probably thrown out of fromCallback(). if (rejectMessageOnUnhandledException) { - getLog().onErrorEvent("Rejecting message: " + t + ": " + getMessageToLog(message)); + String messageToLog = getMessageToLog(message); + getLog().onInvalidMessage(messageToLog, "Rejecting message: " + e.getMessage()); if (resetOrDisconnectIfRequired(message)) { + getLog().onErrorEvent(ErrorEventReasons.DISCONNECT_FOLLOWING_ERROR, "Disconnecting following unhandled exception: " + e + ": " + messageToLog); return; } if (!(MessageUtils.isAdminMessage(msgType)) @@ -1222,7 +1292,8 @@ ignore the message and let the problem correct itself (optimistic approach). // 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); + getLog().onInvalidMessage(getMessageToLog(message), "Unknown error: " + e.getMessage()); + throw new RuntimeError(e); } } @@ -1235,11 +1306,21 @@ ignore the message and let the problem correct itself (optimistic approach). } } + private void throwOrLog(Message message, String reason, Exception exception) throws Exception { + if (rejectInvalidMessage && !Message.WeakParsingMode.FALLBACK.equals(weakParsingMode)) { + throw exception; + } else { + String messageToLog = getMessageToLog(message); + getLog().onErrorEvent(ErrorEventReasons.INVALID_MESSAGE, reason + ": " + messageToLog); + getLog().onInvalidMessage(messageToLog, reason); + } + } + private void handleExceptionAndRejectMessage(final String msgType, final Message message, final HasFieldAndReason e) throws FieldNotFound, IOException { if (MsgType.LOGON.equals(msgType)) { logoutWithErrorMessage(e.getMessage()); } else { - getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + getMessageToLog(message)); + getLog().onErrorEvent(ErrorEventReasons.REJECTING_INVALID_MESSAGE, "Rejecting invalid message: " + e + ": " + getMessageToLog(message)); generateReject(message, e.getMessage(), e.getSessionRejectReason(), e.getField()); } } @@ -1254,7 +1335,9 @@ private void logoutWithErrorMessage(final String reason) throws IOException { private boolean logErrorAndDisconnectIfRequired(final Exception e, Message message) { final boolean resetOrDisconnectIfRequired = resetOrDisconnectIfRequired(message); if (resetOrDisconnectIfRequired) { - getLog().onErrorEvent("Encountered invalid message: " + e + ": " + getMessageToLog(message)); + String messageToLog = getMessageToLog(message); + getLog().onErrorEvent(ErrorEventReasons.INVALID_MESSAGE, "Encountered invalid message: " + e + ": " + messageToLog); + getLog().onInvalidMessage(messageToLog, "Encountered invalid message: " + e); } return resetOrDisconnectIfRequired; } @@ -1284,8 +1367,8 @@ private boolean resetOrDisconnectIfRequired(Message msg) { return false; } if (resetOnError) { - getLog().onErrorEvent("Auto reset"); - reset(); + getLog().onErrorEvent(ErrorEventReasons.RESETTING_ON_ERROR,"Auto reset"); + reset("Error occurred"); return true; } if (disconnectOnError) { @@ -1351,7 +1434,12 @@ private void manageGapFill(Message messageOutSync, int beginSeqNo, int endSeqNo) // 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 + + boolean isFix42orLaterOrFix41withFlag = + (beginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0) || + (fix41ResendRequestAsFix42 && beginString.compareTo(FixVersions.BEGINSTRING_FIX41) == 0); + + if (isFix42orLaterOrFix41withFlag && endSeqNo == 0 || beginString.compareTo(FixVersions.BEGINSTRING_FIX42) <= 0 && endSeqNo == 999999 || endSeqNo >= expectedSenderNum) { endSeqNo = expectedSenderNum - 1; @@ -1398,7 +1486,7 @@ private void generateSequenceReset(Message receivedMessage, int beginSeqNo, int MsgType.SEQUENCE_RESET); final Header header = sequenceReset.getHeader(); header.setBoolean(PossDupFlag.FIELD, true); - initializeHeader(header); + initializeHeader(sequenceReset); header.setUtcTimeStamp(OrigSendingTime.FIELD, header.getUtcTimeStamp(SendingTime.FIELD), getTimestampPrecision()); header.setInt(MsgSeqNum.FIELD, beginSeqNo); @@ -1410,7 +1498,7 @@ private void generateSequenceReset(Message receivedMessage, int beginSeqNo, int receivedMessage.getHeader().getInt(MsgSeqNum.FIELD)); } catch (final FieldNotFound e) { // should not happen as MsgSeqNum must be present - getLog().onErrorEvent("Received message without MsgSeqNum " + getMessageToLog(receivedMessage)); + getLog().onErrorEvent(ErrorEventReasons.MESSAGE_MISSING_MSGSEQNUM, "Received message without MsgSeqNum " + getMessageToLog(receivedMessage)); } } sendRaw(sequenceReset, beginSeqNo); @@ -1422,7 +1510,7 @@ private boolean resendApproved(Message message) throws FieldNotFound { application.toApp(message, sessionID); } catch (final DoNotSend e) { return false; - } catch (final Throwable t) { + } catch (final Exception t) { // Any exception other than DoNotSend will not stop the message from being resent logApplicationException("toApp() during resend", t); } @@ -1435,11 +1523,11 @@ private void initializeResendFields(Message message) throws FieldNotFound { final LocalDateTime sendingTime = header.getUtcTimeStamp(SendingTime.FIELD); header.setUtcTimeStamp(OrigSendingTime.FIELD, sendingTime, getTimestampPrecision()); header.setBoolean(PossDupFlag.FIELD, true); - insertSendingTime(header); + insertSendingTime(message); } private void logApplicationException(String location, Throwable t) { - logThrowable(getLog(), "Application exception in " + location, t); + logThrowable(getLog(), ErrorEventReasons.APPLICATION_ERROR, "Application exception in " + location, t); } private void nextLogout(Message logout) throws IOException, RejectLogon, FieldNotFound, @@ -1484,15 +1572,15 @@ public void generateLogout() { generateLogout(null, null, null); } - private void generateLogout(Message otherLogout) { + public void generateLogout(Message otherLogout) { generateLogout(otherLogout, null, null); } - private void generateLogout(String reason) { + public void generateLogout(String reason) { generateLogout(null, reason, null); } - private void generateLogout(String reason, SessionStatus sessionStatus) { + public void generateLogout(String reason, SessionStatus sessionStatus) { generateLogout(null, reason, sessionStatus); } @@ -1504,7 +1592,7 @@ private void generateLogout(String reason, SessionStatus sessionStatus) { */ private void generateLogout(Message otherLogout, String text, SessionStatus sessionStatus) { final Message logout = messageFactory.create(sessionID.getBeginString(), MsgType.LOGOUT); - initializeHeader(logout.getHeader()); + initializeHeader(logout); if (text != null && !"".equals(text)) { logout.setString(Text.FIELD, text); } @@ -1517,7 +1605,7 @@ private void generateLogout(Message otherLogout, String text, SessionStatus sess otherLogout.getHeader().getInt(MsgSeqNum.FIELD)); } catch (final FieldNotFound e) { // should not happen as MsgSeqNum must be present - getLog().onErrorEvent("Received logout without MsgSeqNum"); + getLog().onErrorEvent(ErrorEventReasons.MESSAGE_MISSING_MSGSEQNUM, "Received logout without MsgSeqNum"); } } sendRaw(logout, 0); @@ -1565,7 +1653,7 @@ private void nextSequenceReset(Message sequenceReset) throws IOException, Reject state.getMessageQueue().dequeueMessagesUpTo(newSequence); } else if (newSequence < getExpectedTargetNum()) { - getLog().onErrorEvent( + getLog().onErrorEvent(ErrorEventReasons.INVALID_SEQUENCE_RESET, "Invalid SequenceReset: newSequence=" + newSequence + " < expected=" + getExpectedTargetNum()); if (resetOrDisconnectIfRequired(sequenceReset)) { @@ -1583,7 +1671,7 @@ private void generateReject(Message message, String str) throws FieldNotFound, I final Header header = message.getHeader(); reject.reverseRoute(header); - initializeHeader(reject.getHeader()); + initializeHeader(reject); final String msgType = (header.isSetField(MsgType.FIELD) ? header.getString(MsgType.FIELD) : null); final String msgSeqNum = (header.isSetField(MsgSeqNum.FIELD) ? header.getString(MsgSeqNum.FIELD) : NumbersCache.get(0)); @@ -1600,7 +1688,9 @@ private void generateReject(Message message, String str) throws FieldNotFound, I reject.setString(Text.FIELD, str); sendRaw(reject, 0); - getLog().onErrorEvent("Reject sent for message " + msgSeqNum + ": " + str); + String logReason = "Reject sent for message " + msgSeqNum + ": " + str; + getLog().onErrorEvent(ErrorEventReasons.SENDING_REJECT, logReason); + getLog().onInvalidMessage(getMessageToLog(message), logReason); } private boolean isPossibleDuplicate(Message message) throws FieldNotFound { @@ -1632,7 +1722,7 @@ private void generateReject(Message message, String text, int err, int field) th final Header header = message.getHeader(); reject.reverseRoute(header); - initializeHeader(reject.getHeader()); + initializeHeader(reject); String msgType = ""; if (header.isSetField(MsgType.FIELD)) { @@ -1683,12 +1773,17 @@ private void generateReject(Message message, String text, int err, int field) th final String logMessage = "Reject sent for message " + msgSeqNum; if (reason != null && (field > 0 || err == SessionRejectReason.INVALID_TAG_NUMBER)) { setRejectReason(reject, field, reason, true); - getLog().onErrorEvent(logMessage + ": " + reason + (reason.endsWith("" + field) ? "" : ":" + field)); + String extendedLogMessage = logMessage + ": " + reason; + getLog().onErrorEvent(ErrorEventReasons.INVALID_MESSAGE, extendedLogMessage); + getLog().onInvalidMessage(getMessageToLog(message), extendedLogMessage); } else if (reason != null) { setRejectReason(reject, reason); - getLog().onErrorEvent(logMessage + ": " + reason); + String extendedLogMessage = logMessage + ": " + reason; + getLog().onErrorEvent(ErrorEventReasons.INVALID_MESSAGE, extendedLogMessage); + getLog().onInvalidMessage(getMessageToLog(message), extendedLogMessage); } else { - getLog().onErrorEvent(logMessage); + getLog().onErrorEvent(ErrorEventReasons.INVALID_MESSAGE, logMessage); + getLog().onInvalidMessage(getMessageToLog(message), logMessage); } if (enableLastMsgSeqNumProcessed) { @@ -1730,7 +1825,7 @@ private void generateBusinessReject(Message message, int err, int field) throws MsgType.BUSINESS_MESSAGE_REJECT); final Header header = message.getHeader(); reject.reverseRoute(header); - initializeHeader(reject.getHeader()); + initializeHeader(reject); final String msgType = header.getString(MsgType.FIELD); final String msgSeqNum = header.getString(MsgSeqNum.FIELD); @@ -1741,10 +1836,10 @@ private void generateBusinessReject(Message message, int err, int field) throws 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) : "")); - + String logReason = "Reject sent for message " + msgSeqNum + (reason != null ? (": " + reason) : "") + + (field != 0 ? (": tag=" + field) : ""); + getLog().onErrorEvent(ErrorEventReasons.SENDING_BUSINESS_REJECT, logReason); + getLog().onInvalidMessage(getMessageToLog(message), logReason); sendRaw(reject, 0); } @@ -1762,7 +1857,7 @@ private void nextTestRequest(Message testRequest) throws FieldNotFound, RejectLo private void generateHeartbeat(Message testRequest) throws FieldNotFound { final Message heartbeat = messageFactory.create(sessionID.getBeginString(), MsgType.HEARTBEAT); - initializeHeader(heartbeat.getHeader()); + initializeHeader(heartbeat); if (testRequest.isSetField(TestReqID.FIELD)) { heartbeat.setString(TestReqID.FIELD, testRequest.getString(TestReqID.FIELD)); } @@ -1835,12 +1930,15 @@ private boolean verify(Message msg, boolean checkTooHigh, boolean checkTooLow) final ResendRange range; synchronized (state.getLock()) { range = state.getResendRange(); - if (msgSeqNum >= range.getEndSeqNo()) { + if (msgSeqNum >= range.getEndSeqNo() || isSatisfyingSequenceReset(msg, range)) { getLog().onEvent( "ResendRequest for messages FROM " + range.getBeginSeqNo() + " TO " + range.getEndSeqNo() + " has been satisfied."); stateListener.onResendRequestSatisfied(sessionID, range.getBeginSeqNo(), range.getEndSeqNo()); state.setResendRange(0, 0, 0); + if (sessionResendListener != null) { + sessionResendListener.onResendRequestSatisfied(sessionID, range.getBeginSeqNo(), range.getEndSeqNo()); + } } } if (msgSeqNum < range.getEndSeqNo() && range.isChunkedResendRequest() && msgSeqNum >= range.getCurrentEndSeqNo()) { @@ -1848,10 +1946,10 @@ private boolean verify(Message msg, boolean checkTooHigh, boolean checkTooLow) sendResendRequest(beginString, range.getEndSeqNo() + 1, msgSeqNum + 1, range.getEndSeqNo()); } } - } catch (final FieldNotFound e) { + } catch (final FieldNotFound | FieldException e) { throw e; } catch (final Exception e) { - getLog().onErrorEvent(e.getClass().getName() + " " + e.getMessage()); + getLog().onErrorEvent(ErrorEventReasons.VERIFY_FAILED, e.getClass().getName() + " " + e.getMessage()); disconnect("Verifying message failed: " + e, true); return false; } @@ -1860,6 +1958,19 @@ private boolean verify(Message msg, boolean checkTooHigh, boolean checkTooLow) return true; } + private boolean isSatisfyingSequenceReset(Message message, ResendRange resendRange) throws FieldNotFound { + if (isSequenceReset(message)) { + final int newSequence = message.getInt(NewSeqNo.FIELD); + return newSequence >= resendRange.getEndSeqNo(); + } else { + return false; + } + } + + private boolean isSequenceReset(Message message) throws FieldNotFound { + return MsgType.SEQUENCE_RESET.equals(message.getHeader().getString(MsgType.FIELD)); + } + private boolean doTargetTooLow(Message msg) throws FieldNotFound, IOException { if (!isPossibleDuplicate(msg)) { final int msgSeqNum = msg.getHeader().getInt(MsgSeqNum.FIELD); @@ -1873,7 +1984,9 @@ private boolean doTargetTooLow(Message msg) throws FieldNotFound, IOException { private void doBadCompID(Message msg) throws IOException, FieldNotFound { if (!MsgType.LOGON.equals(msg.getHeader().getString(MsgType.FIELD))) { - generateReject(msg, BAD_COMPID_REJ_REASON, 0); + if (!MsgType.REJECT.equals(msg.getHeader().getString(MsgType.FIELD))) { + generateReject(msg, BAD_COMPID_REJ_REASON, 0); + } generateLogout(BAD_COMPID_TEXT); } else { logoutWithErrorMessage(BAD_COMPID_TEXT); @@ -1883,7 +1996,11 @@ private void doBadCompID(Message msg) throws IOException, FieldNotFound { private void doBadTime(Message msg) throws IOException, FieldNotFound { try { if (!MsgType.LOGON.equals(msg.getHeader().getString(MsgType.FIELD))) { - generateReject(msg, BAD_TIME_REJ_REASON, SendingTime.FIELD); + if (this.rejectMessageOutOfTime) { + if (!MsgType.REJECT.equals(msg.getHeader().getString(MsgType.FIELD))) { + generateReject(msg, BAD_TIME_REJ_REASON, SendingTime.FIELD); + } + } generateLogout(BAD_TIME_TEXT); } else { logoutWithErrorMessage(BAD_TIME_TEXT); @@ -1957,7 +2074,7 @@ public void next() throws IOException { lastSessionTimeCheck = now; if (!isSessionTime()) { if (state.isResetNeeded() && !state.isResetStatePending()) { - reset(); // only reset if seq nums are != 1 and not isResetStatePending is set + reset("Out of session time"); // only reset if seq nums are != 1 } else if (state.isLogoutTimedOut()) { disconnect("Timed out waiting for logout response", true); } @@ -1988,7 +2105,7 @@ public void next() throws IOException { if (generateLogon()) { getLog().onEvent("Initiated logon request"); } else { - getLog().onErrorEvent("Error during logon request initiation"); + getLog().onErrorEvent(ErrorEventReasons.LOGON_REQUEST_FAILURE, "Error during logon request initiation"); } } } else if (state.isLogonAlreadySent() && state.isLogonTimedOut()) { @@ -2044,7 +2161,7 @@ private boolean isTimeToGenerateLogon() { public void generateHeartbeat() { final Message heartbeat = messageFactory.create(sessionID.getBeginString(), MsgType.HEARTBEAT); - initializeHeader(heartbeat.getHeader()); + initializeHeader(heartbeat); sendRaw(heartbeat, 0); } @@ -2052,7 +2169,7 @@ public void generateTestRequest(String id) { state.incrementTestRequestCounter(); final Message testRequest = messageFactory.create(sessionID.getBeginString(), MsgType.TEST_REQUEST); - initializeHeader(testRequest.getHeader()); + initializeHeader(testRequest); testRequest.setString(TestReqID.FIELD, id); sendRaw(testRequest, 0); } @@ -2085,7 +2202,7 @@ private boolean generateLogon() throws IOException { } setLogonTags(logon); - return sendRaw(logon, 0); + return sendRaw(logon, 0).getOriginalResult(); } /** @@ -2114,10 +2231,12 @@ public void disconnect(String reason, boolean logError) throws IOException { } final String msg = "Disconnecting: " + reason; if (logError) { - getLog().onErrorEvent(msg); + getLog().onErrorEvent(ErrorEventReasons.DISCONNECT_FOLLOWING_ERROR, msg); } else { getLog().onEvent(msg); } + getLog().onDisconnect(reason); + responder.disconnect(); setResponder(null); } @@ -2125,7 +2244,7 @@ public void disconnect(String reason, boolean logError) throws IOException { if (logonReceived || logonSent) { try { application.onLogout(sessionID); - } catch (final Throwable t) { + } catch (final Exception | AssertionError t) { logApplicationException("onLogout()", t); } @@ -2308,7 +2427,7 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre if (isLoggedOn()) { try { application.onLogon(sessionID); - } catch (final Throwable t) { + } catch (final Exception | AssertionError t) { logApplicationException("onLogon()", t); } stateListener.onLogon(sessionID); @@ -2320,71 +2439,160 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre private void resendMessages(Message receivedMessage, int beginSeqNo, int endSeqNo) throws IOException, InvalidMessage, FieldNotFound { - final ArrayList messages = new ArrayList<>(); - 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; boolean appMessageJustSent = false; - for (final String message : messages) { - appMessageJustSent = false; - final Message msg; - try { - // QFJ-626 - msg = parseMessage(message); - msgSeqNum = msg.getHeader().getInt(MsgSeqNum.FIELD); - } catch (final Exception e) { - getLog().onErrorEvent( - "Error handling ResendRequest: failed to parse message (" + e.getMessage() - + "): " + message); - // Note: a SequenceReset message will be generated to fill the gap - continue; - } + if (endSeqNo - beginSeqNo > maxResendBatchRetrievalSize) { + int curBatchStartSeqNo = beginSeqNo; + while (curBatchStartSeqNo <= endSeqNo) { + int endCurBatchSeqNo = endSeqNo; + if (curBatchStartSeqNo + maxResendBatchRetrievalSize < endSeqNo) { + endCurBatchSeqNo = curBatchStartSeqNo + maxResendBatchRetrievalSize; + } + final ArrayList messages = new ArrayList<>(); + try { + state.get(curBatchStartSeqNo, endCurBatchSeqNo, 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); + heartbeat.getHeader().setInt(MsgSeqNum.FIELD, i); + messages.add(heartbeat.toString()); + } + } else { + throw e; + } + } + for (final String message : messages) { + appMessageJustSent = false; + final Message msg; + try { + // QFJ-626 + msg = parseMessage(message); + if (msg.getException() != null) { + getLog().onErrorEvent(ErrorEventReasons.RESEND_REQUEST_PARSER_FAILURE, + "Error handling ResendRequest: failed to parse message (" + msg.getException().getMessage() + + "): " + message); + // Note: a SequenceReset message will be generated to fill the gap + continue; + } + msgSeqNum = msg.getHeader().getInt(MsgSeqNum.FIELD); + } catch (final Exception e) { + getLog().onErrorEvent(ErrorEventReasons.RESEND_REQUEST_PARSER_FAILURE, + "Error handling ResendRequest: failed to parse message (" + e.getMessage() + + "): " + message); + // Note: a SequenceReset message will be generated to fill the gap + continue; + } - if ((current != msgSeqNum) && begin == 0) { - begin = current; - } + if ((current != msgSeqNum) && begin == 0) { + begin = current; + } - final String msgType = msg.getHeader().getString(MsgType.FIELD); + final String msgType = msg.getHeader().getString(MsgType.FIELD); - if (MessageUtils.isAdminMessage(msgType) && !forceResendWhenCorruptedStore) { - if (begin == 0) { - begin = msgSeqNum; + 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; + appMessageJustSent = true; + } else { + if (begin == 0) { + begin = msgSeqNum; + } + } + } + current = msgSeqNum + 1; } - } else { - initializeResendFields(msg); - if (resendApproved(msg)) { - if (begin != 0) { - generateSequenceReset(receivedMessage, begin, msgSeqNum); + curBatchStartSeqNo = endCurBatchSeqNo+1; + } + } else { + final ArrayList messages = new ArrayList<>(); + 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); + heartbeat.getHeader().setInt(MsgSeqNum.FIELD, i); + messages.add(heartbeat.toString()); } - getLog().onEvent("Resending message: " + msgSeqNum); - send(msg.toString()); - begin = 0; - appMessageJustSent = true; } else { + throw e; + } + } + for (final String message : messages) { + appMessageJustSent = false; + final Message msg; + try { + // QFJ-626 + msg = parseMessage(message); + if (msg.getException() != null) { + getLog().onErrorEvent(ErrorEventReasons.RESEND_REQUEST_PARSER_FAILURE, + "Error handling ResendRequest: failed to parse message (" + msg.getException().getMessage() + + "): " + message); + // Note: a SequenceReset message will be generated to fill the gap + continue; + } + msgSeqNum = msg.getHeader().getInt(MsgSeqNum.FIELD); + } catch (final Exception e) { + getLog().onErrorEvent(ErrorEventReasons.RESEND_REQUEST_PARSER_FAILURE, + "Error handling ResendRequest: failed to parse message (" + e.getMessage() + + "): " + message); + // Note: a SequenceReset message will be generated to fill the gap + continue; + } + + 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); + boolean sent = send(msg.toString()); + if (!sent) { + //If we can't send the resend responses we should just stop. Most likely they have disconnected, will reconnect and re-request a smaller range. + getLog().onErrorEvent(ErrorEventReasons.RESEND_REQUEST_SEND_FAILURE,"Failed to send message: " + msgSeqNum + " - ending resend"); + return; + } + begin = 0; + appMessageJustSent = true; + } else { + if (begin == 0) { + begin = msgSeqNum; + } + } } + current = msgSeqNum + 1; } - current = msgSeqNum + 1; } int newBegin = beginSeqNo; @@ -2456,7 +2664,7 @@ private void nextQueued(Message msg, String msgType) throws InvalidMessage, Fiel if (MsgType.LOGON.equals(msgType)) { disconnect(message, true); } else { - getLog().onErrorEvent(message); + getLog().onErrorEvent(ErrorEventReasons.INVALID_MESSAGE, message); if (resetOrDisconnectIfRequired(null)) { return; } @@ -2483,6 +2691,9 @@ private void doTargetTooHigh(Message msg) throws FieldNotFound, IOException, Inv getLog().onEvent( "Already sent ResendRequest FROM: " + range.getBeginSeqNo() + " TO: " + end + ". Not sending another."); + if (sessionResendListener != null) { + sessionResendListener.onDuplicateResendRequested(sessionID, range.getBeginSeqNo(), endSeqNo); + } return; } } @@ -2528,7 +2739,7 @@ private void sendResendRequest(String beginString, int msgSeqNum, int beginSeqNo final Message resendRequest = messageFactory.create(beginString, MsgType.RESEND_REQUEST); resendRequest.setInt(BeginSeqNo.FIELD, beginSeqNo); resendRequest.setInt(EndSeqNo.FIELD, endSeqNo); - initializeHeader(resendRequest.getHeader()); + initializeHeader(resendRequest); sendRaw(resendRequest, 0); getLog().onEvent("Sent ResendRequest FROM: " + beginSeqNo + " TO: " + (endSeqNo == 0 ? "infinity" : endSeqNo)); int resendRangeEndSeqNum = msgSeqNum - 1; @@ -2588,7 +2799,7 @@ private void generateLogon(Message otherLogon, int expectedTargetNum) throws Fie logon.getHeader().setInt(LastMsgSeqNumProcessed.FIELD, otherLogon.getHeader().getInt(MsgSeqNum.FIELD)); } - initializeHeader(logon.getHeader()); + initializeHeader(logon); if (enableNextExpectedMsgSeqNum) { getLog().onEvent("Responding to Logon request with tag 789=" + expectedTargetNum); @@ -2603,14 +2814,89 @@ private void generateLogon(Message otherLogon, int expectedTargetNum) throws Fie state.setLogonSent(true); } - private void persist(Header header, String messageString, int num) throws IOException, FieldNotFound { - if (num == 0) { + private boolean persist(int msgSeqNum, String messageString, int num) throws IOException, FieldNotFound { + boolean result = true; + if (num == 0) { if (persistMessages) { - final int msgSeqNum = header.getInt(MsgSeqNum.FIELD); - state.set(msgSeqNum, messageString); + result = state.set(msgSeqNum, messageString); } state.incrNextSenderMsgSeqNum(); - } + } + return result; + } + + /** + * Added by Flextrade + * Attempt to send the message exactly as provided + * + * @param message is the message to send + * @return Status of the send + */ + private SendResult sendRawExact(IMessage message) { + // 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 String msgType = message.getHeaderString(MsgType.FIELD); + + String messageString; + + boolean persistResult; + if (message.isAdmin()) { + try { + application.toAdmin(message, sessionID); + } catch (final Exception t) { + logApplicationException("toAdmin()", t); + } + + messageString = message.toString(); + try { + final int msgSeqNum = message.getHeaderInt(MsgSeqNum.FIELD); + persistResult = persist(msgSeqNum, messageString, 0); + } catch (final Exception t) { + logApplicationException("toAdmin()", t); + persistResult = false; + } + + if (MsgType.LOGON.equals(msgType) || MsgType.LOGOUT.equals(msgType) + || MsgType.RESEND_REQUEST.equals(msgType) + || MsgType.SEQUENCE_RESET.equals(msgType) || isLoggedOn()) { + result = send(messageString); + } + } else { + try { + application.toApp(message, sessionID); + } catch (final DoNotSend e) { + return SendResult.DO_NOT_SEND; + } catch (final Exception t) { + logApplicationException("toApp()", t); + } + messageString = message.toString(); + try { + final int msgSeqNum = message.getHeaderInt(MsgSeqNum.FIELD); + persistResult = persist(msgSeqNum, messageString, 0); + } catch (final Exception t) { + logApplicationException("toAdmin()", t); + persistResult = false; + } + if (isLoggedOn()) { + result = send(messageString); + } + } + + if (result) { + return persistResult ? SendResult.PERSISTED_SENT : SendResult.NOT_PERSISTED_SENT; + } else { + return persistResult ? SendResult.PERSISTED_NOT_SENT : SendResult.NOT_PERSISTED_NOT_SENT; + } + } catch (final FieldNotFound e) { + logThrowable(state.getLog(), ErrorEventReasons.REQUIRED_FIELD_MISSING, "Error accessing message fields", e); + return SendResult.NOT_PERSISTED_NOT_SENT; + } finally { + state.unlockSenderMsgSeqNum(); + } } /** @@ -2618,36 +2904,36 @@ private void persist(Header header, String messageString, int num) throws IOExce * * @param message is the message to send * @param num is the seq num of the message to send, if 0, the next expected sender seqnum is used. - * @return + * @return Status of the send */ - private boolean sendRaw(Message message, int num) { + private SendResult sendRaw(IMessage 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); + final String msgType = message.getHeaderString(MsgType.FIELD); - initializeHeader(header); + initializeHeader(message); if (num > 0) { - header.setInt(MsgSeqNum.FIELD, num); + message.setHeaderInt(MsgSeqNum.FIELD, num); } if (enableLastMsgSeqNumProcessed) { - if (!header.isSetField(LastMsgSeqNumProcessed.FIELD)) { - header.setInt(LastMsgSeqNumProcessed.FIELD, getExpectedTargetNum() - 1); + if (!message.isSetHeaderField(LastMsgSeqNumProcessed.FIELD)) { + message.setHeaderInt(LastMsgSeqNumProcessed.FIELD, getExpectedTargetNum() - 1); } } String messageString; + boolean persistResult; if (message.isAdmin()) { try { application.toAdmin(message, sessionID); - } catch (final Throwable t) { + } catch (final Exception t) { logApplicationException("toAdmin()", t); } @@ -2659,41 +2945,47 @@ private boolean sendRaw(Message message, int num) { } if (resetSeqNumFlag) { resetState(); - message.getHeader().setInt(MsgSeqNum.FIELD, getExpectedSenderNum()); + message.setHeaderInt(MsgSeqNum.FIELD, getExpectedSenderNum()); } state.setResetSent(resetSeqNumFlag); } } messageString = message.toString(); - persist(message.getHeader(), messageString, num); + final int msgSeqNum = message.getHeaderInt(MsgSeqNum.FIELD); + persistResult = persist(msgSeqNum, messageString, num); if (MsgType.LOGON.equals(msgType) || MsgType.LOGOUT.equals(msgType) - || MsgType.RESEND_REQUEST.equals(msgType) - || MsgType.SEQUENCE_RESET.equals(msgType) || isLoggedOn()) { + || MsgType.RESEND_REQUEST.equals(msgType) + || MsgType.SEQUENCE_RESET.equals(msgType) || isLoggedOn()) { result = send(messageString); } } else { try { application.toApp(message, sessionID); } catch (final DoNotSend e) { - return false; - } catch (final Throwable t) { + return SendResult.DO_NOT_SEND; + } catch (final Exception t) { logApplicationException("toApp()", t); } messageString = message.toString(); - persist(message.getHeader(), messageString, num); + final int msgSeqNum = message.getHeaderInt(MsgSeqNum.FIELD); + persistResult = persist(msgSeqNum, messageString, num); if (isLoggedOn()) { result = send(messageString); } } - return result; + if (result) { + return persistResult ? SendResult.PERSISTED_SENT : SendResult.NOT_PERSISTED_SENT; + } else { + return persistResult ? SendResult.PERSISTED_NOT_SENT : SendResult.NOT_PERSISTED_NOT_SENT; + } } catch (final IOException e) { - logThrowable(getLog(), "Error reading/writing in MessageStore", e); - return false; + logThrowable(getLog(), ErrorEventReasons.IO_ERROR, "Error reading/writing in MessageStore", e); + return SendResult.NOT_PERSISTED_NOT_SENT; } catch (final FieldNotFound e) { - logThrowable(state.getLog(), "Error accessing message fields", e); - return false; + logThrowable(state.getLog(), ErrorEventReasons.REQUIRED_FIELD_MISSING, "Error accessing message fields", e); + return SendResult.NOT_PERSISTED_NOT_SENT; } finally { state.unlockSenderMsgSeqNum(); } @@ -2731,7 +3023,7 @@ private void resetState() { * @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) { + public SendResult send(IMessage message) { return send(message, this.allowPosDup); } @@ -2750,17 +3042,37 @@ public boolean send(Message message) { * @param allowPosDup whether to allow PossDupFlag and OrigSendingTime in the message * @return a status flag indicating whether the write to the network layer was successful. */ - public boolean send(Message message, boolean allowPosDup) { + public SendResult send(IMessage message, boolean allowPosDup) { // Send message as is if allowPosDup flag is set if (allowPosDup) { return sendRaw(message, 0); } - message.getHeader().removeField(PossDupFlag.FIELD); - message.getHeader().removeField(OrigSendingTime.FIELD); + message.removeHeaderField(PossDupFlag.FIELD); + message.removeHeaderField(OrigSendingTime.FIELD); + return sendRaw(message, 0); + } + + /** + * Added by FlexTrade + * + * For cached messages with tag 43 set we want to retain the value of tag 43 + * when sending the messages out regardless of the AllowPosDup flag + */ + public SendResult sendNoChange(IMessage message) { return sendRaw(message, 0); } + /** + * Added by FlexTrade + * + * Attempt to send a message exactly as provided, without any + * modification of its fields + */ + public boolean sendExact(IMessage message) { + return sendRawExact(message).getOriginalResult(); + } + private boolean send(String messageString) { getLog().onOutgoing(messageString); Responder responder; @@ -2798,6 +3110,10 @@ public DataDictionaryProvider getDataDictionaryProvider() { return dataDictionaryProvider; } + public ValidationSettings getValidationSettings() { + return validationSettings; + } + public SessionID getSessionID() { return sessionID; } @@ -2945,7 +3261,7 @@ public String toString() { s += "[in:" + state.getNextTargetMsgSeqNum() + ",out:" + state.getNextSenderMsgSeqNum() + "]"; } catch (final IOException e) { - LogUtil.logThrowable(sessionID, e.getMessage(), e); + LogUtil.logThrowable(sessionID, ErrorEventReasons.GET_NEXT_SEQ_NUM_FAILURE, e.getMessage(), e); } return s; } @@ -3073,6 +3389,10 @@ public void setAllowPosDup(boolean allowPosDup) { this.allowPosDup = allowPosDup; } + public void setFix41ResendRequestAsFix42(boolean fix41ResendRequestAsFix42) { + this.fix41ResendRequestAsFix42 = fix41ResendRequestAsFix42; + } + /** * Closes session resources and unregisters session. This is for internal * use and should typically not be called by an user application. @@ -3093,11 +3413,15 @@ private void closeIfCloseable(Object resource) throws IOException { private void resetIfSessionNotCurrent(SessionID sessionID, long time) throws IOException { if (!isCurrentSession(time)) { - getLog().onEvent("Session state is not current; resetting " + sessionID); - reset(); + getLog().onEvent("Session state is not current ("+currentSessionDetails()+"); resetting " + sessionID); + reset("Session state is not current"); } } + private String currentSessionDetails() throws IOException { + return state.getCreationTimeCalendar().getTime().toString(); + } + private String getMessageToLog(final Message message) { return (message.toRawString() != null ? message.toRawString() : message.toString()); } @@ -3119,4 +3443,31 @@ private void refreshState() throws IOException { stateListener.onRefresh(sessionID); } + public void setUseDictionaryOrdering(boolean useDictionaryOrdering) { + this.useDictionaryOrdering = useDictionaryOrdering; + } + + public boolean getUseDictionaryOrdering() { + return useDictionaryOrdering; + } + + public void setSessionResendListener(SessionResendListener sessionResendListener) { + this.sessionResendListener = sessionResendListener; + } + + public void setIgnoredGarbledMessageListener(IgnoredGarbledMessageListener ignoredGarbledMessageListener) { + this.ignoredGarbledMessageListener = ignoredGarbledMessageListener; + } + + public Message.WeakParsingMode getWeakParsingMode() { + return weakParsingMode; + } + + public boolean useDictionaryForMsgType(String msgType) { + return msgTypesToUseDictionary.isEmpty() || msgTypesToUseDictionary.contains(msgType); + } + + public boolean getRejectMessageOutOfTime() { + return rejectMessageOutOfTime; + } } diff --git a/quickfixj-core/src/main/java/quickfix/SessionID.java b/quickfixj-core/src/main/java/quickfix/SessionID.java index 59c9d875d2..9b631cf20b 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionID.java +++ b/quickfixj-core/src/main/java/quickfix/SessionID.java @@ -182,14 +182,21 @@ private String createID() { return beginString + ":" + senderCompID + /** TODO: FIXHUB-938 + * We currently only distinguish session based on SenderCompID & TargetCompID + (isSet(senderSubID) ? "/" + senderSubID : "") + (isSet(senderLocationID) ? "/" + senderLocationID : "") + **/ + "->" + targetCompID + /** TODO: FIXHUB-938 + * We currently only distinguish session based on SenderCompID & TargetCompID + (isSet(targetSubID) ? "/" + targetSubID : "") + (isSet(targetLocationID) ? "/" + targetLocationID : "") + (sessionQualifier != null && !sessionQualifier.equals(NOT_SET) ? ":" - + sessionQualifier : NOT_SET); + + sessionQualifier : NOT_SET) + **/ + ; } private boolean isSet(String value) { diff --git a/quickfixj-core/src/main/java/quickfix/SessionResendListener.java b/quickfixj-core/src/main/java/quickfix/SessionResendListener.java new file mode 100644 index 0000000000..754e92f00c --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/SessionResendListener.java @@ -0,0 +1,7 @@ +package quickfix; + +public interface SessionResendListener { + void onDuplicateResendRequested(SessionID sessionID, int fromSeqNo, int toSeqNo); + + void onResendRequestSatisfied(SessionID sessionID, int fromSeqNo, int toSeqNo); +} diff --git a/quickfixj-core/src/main/java/quickfix/SessionSettings.java b/quickfixj-core/src/main/java/quickfix/SessionSettings.java index 319e44e1d2..fab2541830 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionSettings.java +++ b/quickfixj-core/src/main/java/quickfix/SessionSettings.java @@ -278,7 +278,7 @@ public long getLong(SessionID sessionID, String key) throws ConfigError, FieldCo try { return Long.parseLong(getString(sessionID, key)); } catch (final NumberFormatException e) { - throw new FieldConvertError(e.getMessage()); + throw new FieldConvertError("Unable to parse setting "+key+" for session_"+ sessionID + ":" +e.getMessage(), e); } } diff --git a/quickfixj-core/src/main/java/quickfix/SessionState.java b/quickfixj-core/src/main/java/quickfix/SessionState.java index ed1e995daf..e93972f53b 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionState.java +++ b/quickfixj-core/src/main/java/quickfix/SessionState.java @@ -20,6 +20,7 @@ package quickfix; import java.io.IOException; +import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -490,19 +491,36 @@ public Object getLock() { return lock; } + public Calendar getCreationTimeCalendar() throws IOException { + return messageStore.getCreationTimeCalendar(); + } + private final static class NullLog implements Log { + @Override public void onOutgoing(String message) { } + @Override public void onIncoming(String message) { } + @Override public void onEvent(String text) { } - public void onErrorEvent(String text) { + @Override + public void onErrorEvent(String category, String text) { + } + + @Override + public void onInvalidMessage(String message, String failureReason) { + } + + @Override + public void onDisconnect(String reason) { } + @Override public void clear() { } } diff --git a/quickfixj-core/src/main/java/quickfix/SessionStateListener.java b/quickfixj-core/src/main/java/quickfix/SessionStateListener.java index 4c1f0ea594..71b74d0930 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionStateListener.java +++ b/quickfixj-core/src/main/java/quickfix/SessionStateListener.java @@ -38,7 +38,7 @@ default void onConnectException(SessionID sessionID, Exception exception) { /** * Called when connection has been disconnected. */ - default void onDisconnect(SessionID sessionID) { + default void onDisconnect(SessionID sessionID, String reason) { } /** diff --git a/quickfixj-core/src/main/java/quickfix/SimpleMessage.java b/quickfixj-core/src/main/java/quickfix/SimpleMessage.java new file mode 100644 index 0000000000..ec94a97873 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/SimpleMessage.java @@ -0,0 +1,266 @@ +package quickfix; + +import quickfix.field.BodyLength; +import quickfix.field.CheckSum; +import quickfix.field.MsgType; +import quickfix.field.SessionRejectReason; +import quickfix.field.converter.BooleanConverter; +import quickfix.field.converter.IntConverter; +import quickfix.field.converter.UtcTimestampConverter; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class SimpleMessage implements IMessage { + private static final List STRICT_ORDERING = Arrays.asList(new Integer[]{8, 9, 35}); + private static final String SOH = String.valueOf('\001'); + private static final String BODY_LENGTH_FIELD = SOH + String.valueOf(BodyLength.FIELD) + '='; + private static final String CHECKSUM_FIELD = SOH + String.valueOf(CheckSum.FIELD) + '='; + private final String messageData; + + public static class TagPair { + public final int tag; + public String value; + + public TagPair(int tag, String value) { + this.tag = tag; + this.value = value; + } + + public String asString() { + return tag+"="+value+SOH; + } + } + + private final List fields; + + public SimpleMessage(String message) { + messageData = message; + fields = Arrays.stream(message.split("\u0001")).map(p -> { + String[] pairData = p.split("=", 2); + return new TagPair(Integer.parseInt(pairData[0]), pairData[1]); + }).collect(Collectors.toList()); + } + + public List getFields() { + return fields; + } + + @Override + public String toString() { + setHeaderString(BodyLength.FIELD, "100"); + setString(10, "000"); + StringBuilder messageString = buildMessageString(); + setBodyLength(messageString); + setChecksum(messageString); + return messageString.toString(); + } + + private static void setBodyLength(StringBuilder stringBuilder) { + int bodyLengthIndex = stringBuilder.indexOf(BODY_LENGTH_FIELD, 0); + int sohIndex = stringBuilder.indexOf(SOH, bodyLengthIndex + 1); + int checkSumIndex = stringBuilder.lastIndexOf(CHECKSUM_FIELD); + int length = checkSumIndex - sohIndex; + bodyLengthIndex += BODY_LENGTH_FIELD.length(); + stringBuilder.replace(bodyLengthIndex, bodyLengthIndex + 3, NumbersCache.get(length)); + } + + private static void setChecksum(StringBuilder stringBuilder) { + int checkSumIndex = stringBuilder.lastIndexOf(CHECKSUM_FIELD); + int checkSum = 0; + for(int i = checkSumIndex; i-- != 0;) + checkSum += stringBuilder.charAt(i); + String checkSumValue = NumbersCache.get((checkSum + 1) & 0xFF); // better than sum % 256 since it avoids overflow issues + checkSumIndex += CHECKSUM_FIELD.length(); + stringBuilder.replace(checkSumIndex + (3 - checkSumValue.length()), checkSumIndex + 3, checkSumValue); + } + + private StringBuilder buildMessageString() { + StringBuilder message = new StringBuilder(); + //Print strict order tags + for (Integer integer : STRICT_ORDERING) { + TagPair tagPair = getField(integer); + if (tagPair != null) { + message.append(tagPair.asString()); + } + } + //Print unclaimed tags + for (TagPair tagPair : fields) { + if (tagPair.tag != 10 && !STRICT_ORDERING.contains(tagPair.tag)) { + message.append(tagPair.asString()); + } + } + //Print footer tag + TagPair footer = getField(10); + if (footer != null) { + message.append(footer.asString()); + } + return message; + } + + private TagPair getField(int tag) { + for (TagPair field : fields) { + if (field.tag == tag) { + return field; + } + } + return null; + } + + @Override + public String toRawString() { + return messageData; + } + + @Override + public boolean isAdmin() { + if (isSetHeaderField(MsgType.FIELD)) { + try { + final String msgType = getHeaderString(MsgType.FIELD); + return MessageUtils.isAdminMessage(msgType); + } catch (final FieldNotFound e) { + // shouldn't happen + } + } + return false; + } + + @Override + public String getHeaderString(int tag) throws FieldNotFound { + for (TagPair field : fields) { + if (field.tag == tag) { + return field.value; + } + } + throw new FieldNotFound(tag); + } + + @Override + public int getHeaderInt(int tag) throws FieldNotFound { + for (TagPair field : fields) { + if (field.tag == tag) { + return Integer.parseInt(field.value); + } + } + throw new FieldNotFound(tag); + } + + @Override + public void setHeaderString(int tag, String value) { + for (TagPair field : fields) { + if (field.tag == tag) { + field.value = value; + return; + } + } + fields.add(new TagPair(tag, value)); + } + + @Override + public void setString(int tag, String value) { + for (TagPair field : fields) { + if (field.tag == tag) { + field.value = value; + return; + } + } + fields.add(new TagPair(tag, value)); + } + + @Override + public void setHeaderInt(int tag, int value) { + for (TagPair field : fields) { + if (field.tag == tag) { + field.value = Integer.toString(value); + return; + } + } + fields.add(new TagPair(tag, Integer.toString(value))); + } + + @Override + public void setInt(int tag, int value) { + for (TagPair field : fields) { + if (field.tag == tag) { + field.value = Integer.toString(value); + return; + } + } + fields.add(new TagPair(tag, Integer.toString(value))); + } + + @Override + public void setHeaderUtcTimeStamp(int tag, LocalDateTime dateTime, UtcTimestampPrecision precision) { + for (TagPair field : fields) { + if (field.tag == tag) { + field.value = UtcTimestampConverter.convert(dateTime, precision); + return; + } + } + } + + @Override + public boolean isSetHeaderField(int tag) { + for (TagPair field : fields) { + if (field.tag == tag) { + return true; + } + } + return false; + } + + @Override + public boolean isSetField(int tag) { + for (TagPair field : fields) { + if (field.tag == tag) { + return true; + } + } + return false; + } + + @Override + public boolean getBoolean(int tag) throws FieldNotFound { + try { + return BooleanConverter.convert(getString(tag)); + } catch (FieldConvertError e) { + throw newIncorrectDataException(e, tag); + } + } + + @Override + public int getInt(int tag) throws FieldNotFound { + try { + return IntConverter.convert(getString(tag)); + } catch (FieldConvertError e) { + throw newIncorrectDataException(e, tag); + } + } + + @Override + public String getString(int tag) throws FieldNotFound { + for (TagPair field : fields) { + if (field.tag == tag) { + return field.value; + } + } + throw new FieldNotFound(tag); + } + + @Override + public void removeHeaderField(int field) { + fields.removeIf(f -> f.tag == field); + } + + @Override + public FieldException getException() { + return null; + } + + private FieldException newIncorrectDataException(FieldConvertError e, int tag) { + return new FieldException(SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE, + e.getMessage(), tag); + } +} diff --git a/quickfixj-core/src/main/java/quickfix/SleepycatStore.java b/quickfixj-core/src/main/java/quickfix/SleepycatStore.java deleted file mode 100644 index e9e7aae1c2..0000000000 --- a/quickfixj-core/src/main/java/quickfix/SleepycatStore.java +++ /dev/null @@ -1,353 +0,0 @@ -/******************************************************************************* - * 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 ask@quickfixengine.org if any conditions of this licensing - * are not clear to you. - ******************************************************************************/ - -package quickfix; - -import java.io.File; -import java.io.IOException; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; - -import org.quickfixj.CharsetSupport; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.sleepycat.bind.EntryBinding; -import com.sleepycat.bind.tuple.TupleBinding; -import com.sleepycat.bind.tuple.TupleInput; -import com.sleepycat.bind.tuple.TupleOutput; -import com.sleepycat.je.Cursor; -import com.sleepycat.je.Database; -import com.sleepycat.je.DatabaseConfig; -import com.sleepycat.je.DatabaseEntry; -import com.sleepycat.je.DatabaseException; -import com.sleepycat.je.Environment; -import com.sleepycat.je.EnvironmentConfig; -import com.sleepycat.je.LockMode; -import com.sleepycat.je.OperationStatus; - -/** - * Sleepycat message and session state storage. This could be creating - * using the Sleepycat store factory. - * - * @see SleepycatStoreFactory - */ -public class SleepycatStore implements MessageStore { - private final Logger log = LoggerFactory.getLogger(getClass()); - private final SessionID sessionID; // session key - private SessionInfo info; - - private final String dbDir; - private String seqDbName = "seq"; - private String msgDbName = "outmsg"; - - private Database messageDatabase; - private Database sequenceDatabase; - private final SessionIDTupleBinding sessionIDBinding = new SessionIDTupleBinding(); - private final SessionInfoTupleBinding sessionInfoBinding = new SessionInfoTupleBinding(); - private Environment environment; - - private final DatabaseEntry sessionIDKey = new DatabaseEntry(); - private final DatabaseEntry sessionInfoBytes = new DatabaseEntry(); - private final String charsetEncoding = CharsetSupport.getCharset(); - - private static class SessionIDTupleBinding extends TupleBinding { - - /* - * (non-Javadoc) - * - * @see com.sleepycat.bind.tuple.TupleBinding#entryToObject(com.sleepycat.bind.tuple.TupleInput) - */ - public Object entryToObject(TupleInput tupleIn) { - return new SessionID(tupleIn.readString(), tupleIn.readString(), tupleIn.readString(), - tupleIn.readString(), tupleIn.readString(), tupleIn.readString(), tupleIn - .readString(), tupleIn.readString()); - } - - /* - * (non-Javadoc) - * - * @see com.sleepycat.bind.tuple.TupleBinding#objectToEntry(java.lang.Object, - * com.sleepycat.bind.tuple.TupleOutput) - */ - public void objectToEntry(Object object, TupleOutput tupleOut) { - SessionID sessionID = (SessionID) object; - tupleOut.writeString(sessionID.getBeginString()); - tupleOut.writeString(sessionID.getSenderCompID()); - tupleOut.writeString(sessionID.getSenderSubID()); - tupleOut.writeString(sessionID.getSenderLocationID()); - tupleOut.writeString(sessionID.getTargetCompID()); - tupleOut.writeString(sessionID.getTargetSubID()); - tupleOut.writeString(sessionID.getTargetLocationID()); - tupleOut.writeString(sessionID.getSessionQualifier()); - } - } - - private static class SessionInfoTupleBinding extends TupleBinding { - - /* - * (non-Javadoc) - * - * @see com.sleepycat.bind.tuple.TupleBinding#entryToObject(com.sleepycat.bind.tuple.TupleInput) - */ - public Object entryToObject(TupleInput tupleIn) { - return new SessionInfo(SystemTime.getUtcCalendar(new Date(tupleIn.readLong())), tupleIn - .readInt(), tupleIn.readInt()); - } - - /* - * (non-Javadoc) - * - * @see com.sleepycat.bind.tuple.TupleBinding#objectToEntry(java.lang.Object, - * com.sleepycat.bind.tuple.TupleOutput) - */ - public void objectToEntry(Object object, TupleOutput tupleOut) { - SessionInfo sessionInfo = (SessionInfo) object; - tupleOut.writeLong(sessionInfo.getCreationTime().getTimeInMillis()); - tupleOut.writeInt(sessionInfo.getNextSenderMsgSeqNum()); - tupleOut.writeInt(sessionInfo.getNextTargetMsgSeqNum()); - } - } - - private static class SessionInfo { - private int nextSenderMsgSeqNum; - private int nextTargetMsgSeqNum; - private final Calendar creationTime; - - public SessionInfo() { - this(SystemTime.getUtcCalendar(), 1, 1); - } - - public SessionInfo(Calendar creationTime, int nextSenderMsgSeqNum, int nextTargetMsgSeqNum) { - super(); - this.creationTime = creationTime; - this.nextSenderMsgSeqNum = nextSenderMsgSeqNum; - this.nextTargetMsgSeqNum = nextTargetMsgSeqNum; - } - - public Calendar getCreationTime() { - return creationTime; - } - - public int getNextSenderMsgSeqNum() { - return nextSenderMsgSeqNum; - } - - public int getNextTargetMsgSeqNum() { - return nextTargetMsgSeqNum; - } - - //public void setCreationTime(Calendar creationTime) { - // this.creationTime = creationTime; - //} - - public void setNextSenderMsgSeqNum(int nextSenderMsgSeqNum) { - this.nextSenderMsgSeqNum = nextSenderMsgSeqNum; - } - - public void setNextTargetMsgSeqNum(int nextTargetMsgSeqNum) { - this.nextTargetMsgSeqNum = nextTargetMsgSeqNum; - } - } - - public SleepycatStore(SessionID sessionID, String databaseDir, String sequenceDbName, - String messageDbName) throws IOException { - this.sessionID = sessionID; - dbDir = databaseDir; - seqDbName = sequenceDbName; - msgDbName = messageDbName; - open(); - } - - void open() throws IOException { - try { - // Open the environment. Create it if it does not already exist. - EnvironmentConfig envConfig = new EnvironmentConfig(); - envConfig.setAllowCreate(true); - environment = new Environment(new File(dbDir), envConfig); - - DatabaseConfig dbConfig = new DatabaseConfig(); - dbConfig.setAllowCreate(true); - - // Open the database. Create it if it does not already exist. - messageDatabase = environment.openDatabase(null, msgDbName, dbConfig); - sequenceDatabase = environment.openDatabase(null, seqDbName, dbConfig); - - loadSessionInfo(); - } catch (DatabaseException dbe) { - convertToIOExceptionAndRethrow(dbe); - } - } - - void close() throws IOException { - try { - messageDatabase.close(); - sequenceDatabase.close(); - environment.close(); - } catch (DatabaseException e) { - convertToIOExceptionAndRethrow(e); - } - } - - public synchronized void get(int startSequence, int endSequence, Collection messages) - throws IOException { - Cursor cursor = null; - try { - DatabaseEntry sequenceKey = new DatabaseEntry(); - EntryBinding sequenceBinding = TupleBinding.getPrimitiveBinding(Integer.class); - // Must start at start-1 because db will look for next record larger - sequenceBinding.objectToEntry(startSequence - 1, sequenceKey); - - cursor = messageDatabase.openCursor(null, null); - DatabaseEntry messageBytes = new DatabaseEntry(); - OperationStatus retVal = cursor.getSearchKeyRange(sequenceKey, messageBytes, - LockMode.DEFAULT); - - if (retVal == OperationStatus.NOTFOUND) { - log.debug("{}/{} not matched in database {}", sequenceKey, messageBytes, messageDatabase.getDatabaseName()); - } else { - Integer sequenceNumber = (Integer) sequenceBinding.entryToObject(sequenceKey); - while (sequenceNumber <= endSequence) { - messages.add(new String(messageBytes.getData(), charsetEncoding)); - if (log.isDebugEnabled()) { - log.debug("Found record {}=>{} for search key/data: {}=>{}", - sequenceNumber, new String(messageBytes.getData(), charsetEncoding), sequenceKey, messageBytes); - } - cursor.getNext(sequenceKey, messageBytes, LockMode.DEFAULT); - sequenceNumber = (Integer) sequenceBinding.entryToObject(sequenceKey); - } - } - } catch (Exception e) { - convertToIOExceptionAndRethrow(e); - } finally { - try { - if (cursor != null) { - cursor.close(); - } - } catch (DatabaseException dbe) { - convertToIOExceptionAndRethrow(dbe); - } - } - } - - private void convertToIOExceptionAndRethrow(Exception e) throws IOException { - if (e instanceof IOException) { - throw (IOException) e; - } - IOException ioe = new IOException(e.getMessage()); - ioe.setStackTrace(e.getStackTrace()); - throw ioe; - } - - public Date getCreationTime() throws IOException { - return info.getCreationTime().getTime(); - } - - public int getNextSenderMsgSeqNum() throws IOException { - return info.getNextSenderMsgSeqNum(); - } - - public int getNextTargetMsgSeqNum() throws IOException { - return info.getNextTargetMsgSeqNum(); - } - - public void incrNextSenderMsgSeqNum() throws IOException { - info.setNextSenderMsgSeqNum(info.getNextSenderMsgSeqNum() + 1); - storeSessionInfo(); - } - - public void incrNextTargetMsgSeqNum() throws IOException { - info.setNextTargetMsgSeqNum(info.getNextTargetMsgSeqNum() + 1); - storeSessionInfo(); - } - - public void reset() throws IOException { - try { - info = new SessionInfo(); - storeSessionInfo(); - sequenceDatabase.close(); - messageDatabase.close(); - environment.truncateDatabase(null, seqDbName, false); - environment.truncateDatabase(null, msgDbName, false); - environment.close(); - open(); - } catch (DatabaseException e) { - convertToIOExceptionAndRethrow(e); - } - } - - public boolean set(int sequence, String message) throws IOException { - try { - DatabaseEntry sequenceKey = new DatabaseEntry(); - EntryBinding sequenceBinding = TupleBinding.getPrimitiveBinding(Integer.class); - sequenceBinding.objectToEntry(sequence, sequenceKey); - DatabaseEntry messageBytes = new DatabaseEntry(message.getBytes(CharsetSupport.getCharset())); - messageDatabase.put(null, sequenceKey, messageBytes); - } catch (Exception e) { - convertToIOExceptionAndRethrow(e); - } - return true; - } - - public void setNextSenderMsgSeqNum(int next) throws IOException { - info.setNextSenderMsgSeqNum(next); - storeSessionInfo(); - } - - public void setNextTargetMsgSeqNum(int next) throws IOException { - info.setNextTargetMsgSeqNum(next); - storeSessionInfo(); - } - - private void loadSessionInfo() throws IOException { - synchronized (sessionIDKey) { - sessionIDBinding.objectToEntry(sessionID, sessionIDKey); - - try { - sequenceDatabase.get(null, sessionIDKey, sessionInfoBytes, LockMode.DEFAULT); - if (sessionInfoBytes.getSize() > 0) { - info = (SessionInfo) sessionInfoBinding.entryToObject(sessionInfoBytes); - } else { - info = new SessionInfo(); - storeSessionInfo(); - } - } catch (DatabaseException e) { - convertToIOExceptionAndRethrow(e); - } - } - } - - private void storeSessionInfo() throws IOException { - synchronized (sessionIDKey) { - sessionIDBinding.objectToEntry(sessionID, sessionIDKey); - sessionInfoBinding.objectToEntry(info, sessionInfoBytes); - - try { - sequenceDatabase.put(null, sessionIDKey, sessionInfoBytes); - } catch (DatabaseException e) { - convertToIOExceptionAndRethrow(e); - } - } - } - - public void refresh() throws IOException { - loadSessionInfo(); - } -} diff --git a/quickfixj-core/src/main/java/quickfix/SleepycatStoreFactory.java b/quickfixj-core/src/main/java/quickfix/SleepycatStoreFactory.java deleted file mode 100644 index aa7250216c..0000000000 --- a/quickfixj-core/src/main/java/quickfix/SleepycatStoreFactory.java +++ /dev/null @@ -1,66 +0,0 @@ -/******************************************************************************* - * 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 ask@quickfixengine.org if any conditions of this licensing - * are not clear to you. - ******************************************************************************/ - -package quickfix; - -/** - * Message store using the Sleepycat Java Edition database for message and - * sequence number storage. - */ -public class SleepycatStoreFactory implements MessageStoreFactory { - - /** - * Directory path where Sleepycat files are stored. This directory must already - * exist. Required. - */ - public static final String SETTING_SLEEPYCAT_DATABASE_DIR = "SleepycatDatabaseDir"; - - /** - * Database name for the sequence number database. Optional. - */ - public static final String SETTING_SLEEPYCAT_SEQUENCE_DB_NAME = "SleepycatSequenceDbName"; - - /** - * Database name for the message database. Optional. - */ - public static final String SETTING_SLEEPYCAT_MESSAGE_DB_NAME = "SleepycatMessageDbName"; - - private SessionSettings settings = new SessionSettings(); - - public SleepycatStoreFactory(SessionSettings settings) { - this.settings = settings; - } - - public MessageStore create(SessionID sessionID) { - try { - String dbDir = settings.getString(sessionID, SETTING_SLEEPYCAT_DATABASE_DIR); - String seqDbName = "seq"; - if (settings.isSetting(sessionID, SETTING_SLEEPYCAT_SEQUENCE_DB_NAME)) { - seqDbName = settings.getString(sessionID, SETTING_SLEEPYCAT_SEQUENCE_DB_NAME); - } - String msgDbName = "msg"; - if (settings.isSetting(sessionID, SETTING_SLEEPYCAT_MESSAGE_DB_NAME)) { - msgDbName = settings.getString(sessionID, SETTING_SLEEPYCAT_MESSAGE_DB_NAME); - } - return new SleepycatStore(sessionID, dbDir, seqDbName, msgDbName); - } catch (Exception e) { - throw new RuntimeError(e); - } - } -} diff --git a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java index 23a2769448..697107df8a 100644 --- a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java @@ -107,6 +107,7 @@ private void initialize() throws ConfigError { synchronized (isStarted) { if (isStarted.compareAndSet(false, true)) { eventHandlingStrategy.setExecutor(longLivedExecutor); + createSessionAcceptors(); startAcceptingConnections(); eventHandlingStrategy.blockInThread(); } diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java index a1190bb585..906773a992 100644 --- a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java @@ -100,6 +100,7 @@ public ThreadedSocketAcceptor(SessionFactory sessionFactory, SessionSettings set @Override public void start() throws ConfigError, RuntimeError { eventHandlingStrategy.setExecutor(longLivedExecutor); + createSessionAcceptors(); startAcceptingConnections(); } diff --git a/quickfixj-core/src/main/java/quickfix/ToAppListener.java b/quickfixj-core/src/main/java/quickfix/ToAppListener.java index 3de5d1d1a4..e58f57839e 100644 --- a/quickfixj-core/src/main/java/quickfix/ToAppListener.java +++ b/quickfixj-core/src/main/java/quickfix/ToAppListener.java @@ -1,5 +1,5 @@ package quickfix; -public interface ToAppListener { +public interface ToAppListener { void accept(T message, SessionID sessionId) throws DoNotSend; } diff --git a/quickfixj-core/src/main/java/quickfix/ValidationSettings.java b/quickfixj-core/src/main/java/quickfix/ValidationSettings.java new file mode 100644 index 0000000000..dde5e5859a --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/ValidationSettings.java @@ -0,0 +1,104 @@ +package quickfix; + +public class ValidationSettings { + boolean checkFieldsOutOfOrder = true; + boolean checkFieldsHaveValues = true; + boolean checkUserDefinedFields = true; + boolean checkUnorderedGroupFields = true; + boolean allowUnknownMessageFields = false; + boolean useFirstTagAsGroupDelimiter = false; + boolean onlyAllowSeenOrKnownFieldsInLastGroup = false; + + public ValidationSettings() {} + + public ValidationSettings(ValidationSettings validationSettings) { + this.checkFieldsOutOfOrder = validationSettings.checkFieldsOutOfOrder; + this.checkFieldsHaveValues = validationSettings.checkFieldsHaveValues; + this.checkUserDefinedFields = validationSettings.checkUserDefinedFields; + this.checkUnorderedGroupFields = validationSettings.checkUnorderedGroupFields; + this.allowUnknownMessageFields = validationSettings.allowUnknownMessageFields; + this.useFirstTagAsGroupDelimiter = validationSettings.useFirstTagAsGroupDelimiter; + this.onlyAllowSeenOrKnownFieldsInLastGroup = validationSettings.onlyAllowSeenOrKnownFieldsInLastGroup; + } + + /** + * Controls whether out of order fields are checked. + * + * @param flag true = checked, false = not checked + */ + public void setCheckFieldsOutOfOrder(boolean flag) { + checkFieldsOutOfOrder = flag; + } + + public boolean isCheckFieldsOutOfOrder() { + return checkFieldsOutOfOrder; + } + + public boolean isCheckUnorderedGroupFields() { + return checkUnorderedGroupFields; + } + + public boolean isCheckFieldsHaveValues() { + return checkFieldsHaveValues; + } + + public boolean isCheckUserDefinedFields() { + return checkUserDefinedFields; + } + + public boolean isAllowUnknownMessageFields() { + return allowUnknownMessageFields; + } + + public boolean isUseFirstTagAsGroupDelimiter() { + return useFirstTagAsGroupDelimiter; + } + + /** + * Controls whether group fields are in the same order + * + * @param flag true = checked, false = not checked + */ + public void setCheckUnorderedGroupFields(boolean flag) { + checkUnorderedGroupFields = flag; + } + + /** + * Controls whether empty field values are checked. + * + * @param flag true = checked, false = not checked + */ + public void setCheckFieldsHaveValues(boolean flag) { + checkFieldsHaveValues = flag; + } + + /** + * Controls whether user defined fields are checked. + * + * @param flag true = checked, false = not checked + */ + public void setCheckUserDefinedFields(boolean flag) { + checkUserDefinedFields = flag; + } + + public void setAllowUnknownMessageFields(boolean allowUnknownFields) { + allowUnknownMessageFields = allowUnknownFields; + } + + /** + * Controls whether to just use the first tag as the delimiter when parsing a group + * + * @param flag true = checked, false = not checked + */ + public void setUseFirstTagAsGroupDelimiter(boolean flag) { + useFirstTagAsGroupDelimiter = flag; + } + + public boolean isOnlyAllowSeenOrKnownFieldsInLastGroup() { + return onlyAllowSeenOrKnownFieldsInLastGroup; + } + + public void setOnlyAllowSeenOrKnownFieldsInLastGroup(boolean flag) { + onlyAllowSeenOrKnownFieldsInLastGroup = flag; + } +} diff --git a/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java b/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java index 207238d62f..6241117f27 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java +++ b/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java @@ -26,16 +26,7 @@ import org.apache.mina.filter.codec.ProtocolDecoderException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import quickfix.ConfigError; -import quickfix.FieldConvertError; -import quickfix.InvalidMessage; -import quickfix.Log; -import quickfix.LogUtil; -import quickfix.Message; -import quickfix.MessageUtils; -import quickfix.Session; -import quickfix.SessionID; -import quickfix.SessionSettings; +import quickfix.*; import static quickfix.MessageUtils.parse; @@ -89,7 +80,7 @@ public void exceptionCaught(IoSession ioSession, Throwable cause) throws Excepti } disconnectNeeded = true; } else if (realCause instanceof CriticalProtocolCodecException) { - reason = "Critical protocol codec error: " + cause; + reason = "Critical protocol codec erro: " + cause; disconnectNeeded = true; } else if (realCause instanceof ProtocolCodecException) { reason = "Protocol handler exception: " + cause; @@ -109,7 +100,7 @@ public void exceptionCaught(IoSession ioSession, Throwable cause) throws Excepti } } else { if (quickFixSession != null) { - LogUtil.logThrowable(quickFixSession.getLog(), reason, cause); + LogUtil.logThrowable(quickFixSession.getLog(), ErrorEventReasons.IO_ERROR, reason, cause); } else { log.error(reason, cause); } @@ -142,21 +133,25 @@ public void messageReceived(IoSession ioSession, Object message) throws Exceptio sessionLog.onIncoming(messageString); try { Message fixMessage = parse(quickFixSession, messageString); + if (fixMessage.getException() != null) { + sessionLog.onInvalidMessage(messageString, fixMessage.getException().getMessage()); + } processMessage(ioSession, fixMessage); } catch (InvalidMessage e) { if (rejectGarbledMessage) { final Message fixMessage = e.getFixMessage(); if ( fixMessage != null ) { - sessionLog.onErrorEvent("Processing garbled message: " + e.getMessage()); + sessionLog.onErrorEvent(ErrorEventReasons.GARBLED_MESSAGE, "Processing garbled message: " + e.getMessage()); processMessage(ioSession, fixMessage); return; } } if (MessageUtils.isLogon(messageString)) { - sessionLog.onErrorEvent("Invalid LOGON message, disconnecting: " + e.getMessage()); + sessionLog.onErrorEvent(ErrorEventReasons.INVALID_MESSAGE, "Invalid LOGON message, disconnecting: " + e.getMessage()); ioSession.closeNow(); } else { - sessionLog.onErrorEvent("Invalid message: " + e.getMessage()); + sessionLog.onErrorEvent(ErrorEventReasons.INVALID_MESSAGE, "Invalid message: " + e.getMessage()); + sessionLog.onInvalidMessage(messageString, e.getMessage()); } } } else { diff --git a/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java b/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java index f58455343a..9a842210ec 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java +++ b/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java @@ -242,7 +242,7 @@ protected void logoutAllSessions(boolean forceDisconnect) { for (Session session : sessions.values()) { try { session.logout(); - } catch (Throwable e) { + } catch (Exception e) { logError(session.getSessionID(), null, "Error during logout", e); } } @@ -254,7 +254,7 @@ protected void logoutAllSessions(boolean forceDisconnect) { if (session.isLoggedOn()) { session.disconnect("Forcibly disconnecting session", false); } - } catch (Throwable e) { + } catch (Exception e) { logError(session.getSessionID(), null, "Error during disconnect", e); } } @@ -295,8 +295,8 @@ protected void waitForLogout() { } } - protected void logError(SessionID sessionID, IoSession protocolSession, String message, Throwable t) { - log.error(message + getLogSuffix(sessionID, protocolSession), t); + protected void logError(SessionID sessionID, IoSession protocolSession, String message, Exception e) { + log.error(message + getLogSuffix(sessionID, protocolSession), e); } private String getLogSuffix(SessionID sessionID, IoSession protocolSession) { @@ -350,7 +350,7 @@ public void run() { logError(session.getSessionID(), null, "Error in session timer processing", e); } } - } catch (Throwable e) { + } catch (Exception e) { log.error("Error during timer processing", e); } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java b/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java index 4d34d5ca97..e7d32de214 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java +++ b/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java @@ -161,8 +161,8 @@ public SessionMessageEvent(Session session, Message message) { public void processMessage() { try { quickfixSession.next(message); - } catch (Throwable e) { - LogUtil.logThrowable(quickfixSession.getSessionID(), e.getMessage(), e); + } catch (Exception e) { + LogUtil.logThrowable(quickfixSession.getSessionID(), ErrorEventReasons.MESSAGE_PROCESSOR_ERROR, e.getMessage(), e); } } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java b/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java index 90d977a9ce..f5fc4e44f4 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java @@ -202,7 +202,7 @@ public void enqueue(Message message) { try { queueTracker.put(message); } catch (final InterruptedException e) { - quickfixSession.getLog().onErrorEvent(e.toString()); + quickfixSession.getLog().onErrorEvent(ErrorEventReasons.FAILED_TO_QUEUE_MESSAGE, e.toString()); Thread.currentThread().interrupt(); } } @@ -226,11 +226,13 @@ void doRun() { } } catch (final InterruptedException e) { LogUtil.logThrowable(quickfixSession.getSessionID(), + ErrorEventReasons.MESSAGE_DISPATCHER_ERROR, "Message dispatcher interrupted", e); stopping = true; Thread.currentThread().interrupt(); - } catch (final Throwable e) { + } catch (final Exception e) { LogUtil.logThrowable(quickfixSession.getSessionID(), + ErrorEventReasons.MESSAGE_PROCESSOR_ERROR, "Error during message processing", e); } } @@ -240,8 +242,9 @@ void doRun() { for (Message message : tempList) { try { quickfixSession.next(message); - } catch (final Throwable e) { + } catch (final Exception e) { LogUtil.logThrowable(quickfixSession.getSessionID(), + ErrorEventReasons.MESSAGE_PROCESSOR_ERROR, "Error during message processing", e); } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java index 2b7cb2b359..a0276fd6b2 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java @@ -91,39 +91,46 @@ protected AbstractSocketAcceptor(Application application, messageFactory)); } + protected void createSessionAcceptors() throws ConfigError { + createSessions(getSettings(), isContinueInitOnError()); + } + // TODO SYNC Does this method really need synchronization? protected synchronized void startAcceptingConnections() throws ConfigError { - boolean continueInitOnError = isContinueInitOnError(); - createSessions(getSettings(), continueInitOnError); - startSessionTimer(); - + // createSessions(getSettings(), continueInitOnError); SocketAddress address = null; - for (AcceptorSocketDescriptor socketDescriptor : socketDescriptorForAddress.values()) { - try { - address = socketDescriptor.getAddress(); - IoAcceptor ioAcceptor = getIoAcceptor(socketDescriptor); - CompositeIoFilterChainBuilder ioFilterChainBuilder = new CompositeIoFilterChainBuilder(getIoFilterChainBuilder()); - - if (socketDescriptor.isUseSSL()) { - installSSL(socketDescriptor, ioFilterChainBuilder); - } + try { + startSessionTimer(); + for (AcceptorSocketDescriptor socketDescriptor : socketDescriptorForAddress.values()) { + try { + address = socketDescriptor.getAddress(); + IoAcceptor ioAcceptor = getIoAcceptor(socketDescriptor); + CompositeIoFilterChainBuilder ioFilterChainBuilder = new CompositeIoFilterChainBuilder(getIoFilterChainBuilder()); + + if (socketDescriptor.isUseSSL()) { + installSSL(socketDescriptor, ioFilterChainBuilder); + } - ioFilterChainBuilder.addLast(FIXProtocolCodecFactory.FILTER_NAME, + ioFilterChainBuilder.addLast(FIXProtocolCodecFactory.FILTER_NAME, new ProtocolCodecFilter(new FIXProtocolCodecFactory())); - ioAcceptor.setFilterChainBuilder(ioFilterChainBuilder); - ioAcceptor.setCloseOnDeactivation(false); - ioAcceptor.bind(socketDescriptor.getAddress()); - log.info("Listening for connections at {} for session(s) {}", address, socketDescriptor.getAcceptedSessions().keySet()); - } catch (IOException | GeneralSecurityException | ConfigError e) { - if (continueInitOnError) { - log.warn("error during session initialization for session(s) {}, continuing...", socketDescriptor.getAcceptedSessions().keySet(), e); - } else { - log.error("Cannot start acceptor session for {}, error: {}", address, e); - throw new RuntimeError(e); + ioAcceptor.setFilterChainBuilder(ioFilterChainBuilder); + ioAcceptor.setCloseOnDeactivation(false); + ioAcceptor.bind(socketDescriptor.getAddress()); + log.info("Listening for connections at {} for session(s) {}", address, socketDescriptor.getAcceptedSessions().keySet()); + } catch (IOException | GeneralSecurityException | ConfigError e) { + if (continueInitOnError) { + log.warn("error during session initialization for session(s) {}, continuing...", socketDescriptor.getAcceptedSessions().keySet(), e); + } else { + log.error("Cannot start acceptor session for {}, error: {}", address, e); + throw new RuntimeError(e); + } } } + } catch (Exception e) { + log.error("Cannot start acceptor session for {}, error: {}", address, e); + throw new RuntimeError(e); } } @@ -244,7 +251,7 @@ private void createSessions(SessionSettings settings, boolean continueInitOnErro setupSession(settings, sessionID, isTemplate, allSessions); } - } catch (Throwable t) { + } catch (Exception t) { if (continueInitOnError) { log.warn("error during session initialization for {}, continuing...", sessionID, t); } else { diff --git a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AcceptorIoHandler.java b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AcceptorIoHandler.java index ca3e5dab56..e59a426a9d 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AcceptorIoHandler.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AcceptorIoHandler.java @@ -19,14 +19,14 @@ package quickfix.mina.acceptor; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Optional; + import org.apache.mina.core.session.IoSession; -import quickfix.Log; -import quickfix.Message; -import quickfix.MessageUtils; -import quickfix.Responder; -import quickfix.Session; -import quickfix.SessionID; -import quickfix.SessionSettings; +import quickfix.*; import quickfix.field.ApplVerID; import quickfix.field.DefaultApplVerID; import quickfix.field.HeartBtInt; @@ -37,11 +37,6 @@ import quickfix.mina.NetworkingOptions; import quickfix.mina.SessionConnector; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.Optional; - class AcceptorIoHandler extends AbstractIoHandler { private final EventHandlingStrategy eventHandlingStrategy; private final AcceptorSessionProvider sessionProvider; @@ -73,10 +68,16 @@ protected void processMessage(IoSession protocolSession, Message message) throws final Log sessionLog = qfSession.getLog(); Responder responder = qfSession.getResponder(); if (responder != null) { - // Session is already bound to another connection - sessionLog.onErrorEvent("Multiple logons/connections for this session are not allowed." - + " Closing connection from " + protocolSession.getRemoteAddress() - + " since session is already established from " + responder.getRemoteAddress()); + if (responder.getRemoteAddress() == null) { + log.error("Responder for session {} has no remote address. Connection from {} will be closed. Session details:\n{}", + sessionID, protocolSession.getRemoteAddress(), qfSession); + } else { + // Session is already bound to another connection + sessionLog.onErrorEvent(ErrorEventReasons.MULTIPLE_LOGONS, + "Multiple logons/connections for session " + sessionID + " are not allowed." + + " Closing connection from " + protocolSession.getRemoteAddress() + + " since session is already established from " + responder.getRemoteAddress()); + } protocolSession.closeNow(); return; } @@ -101,11 +102,19 @@ protected void processMessage(IoSession protocolSession, Message message) throws } } } else { - log.error("Unknown session ID during logon: {} cannot be found in session list {} (connecting from {} to {})", + ArrayList allSessions = eventHandlingStrategy.getSessionConnector().getSessions(); + if (allSessions.contains(sessionID)) { + log.error("Session with ID {} is attempting to connect on the wrong port (connecting from {} to {})", sessionID, - eventHandlingStrategy.getSessionConnector().getSessions(), protocolSession.getRemoteAddress(), protocolSession.getLocalAddress()); + } else { + log.error("Unknown session ID during logon: {} cannot be found in session list {} (connecting from {} to {})", + sessionID, + allSessions, + protocolSession.getRemoteAddress(), + protocolSession.getLocalAddress()); + } return; } } else { diff --git a/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java index 84f9e142e9..7c38d8a390 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java @@ -220,7 +220,7 @@ private void createSessions(boolean continueInitOnError) throws ConfigError, Fie final Session quickfixSession = createSession(sessionID); initiatorSessions.put(sessionID, quickfixSession); } - } catch (final Throwable e) { + } catch (final Exception e) { if (continueInitOnError) { log.warn("error during session initialization for {}, continuing...", sessionID, e); } else { @@ -255,7 +255,7 @@ private int[] getReconnectIntervalInSeconds(SessionID sessionID) throws ConfigEr if (ret != null) { return ret; } - } catch (final Throwable e) { + } catch (final Exception e) { throw new ConfigError(e); } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java index e6c97eeeb8..ac9e47684f 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java @@ -26,12 +26,7 @@ import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.proxy.ProxyConnector; import org.apache.mina.transport.socket.SocketConnector; -import quickfix.ConfigError; -import quickfix.LogUtil; -import quickfix.Session; -import quickfix.SessionID; -import quickfix.SessionSettings; -import quickfix.SystemTime; +import quickfix.*; import quickfix.mina.CompositeIoFilterChainBuilder; import quickfix.mina.EventHandlingStrategy; import quickfix.mina.NetworkingOptions; @@ -213,8 +208,8 @@ public void run() { } else { pollConnectFuture(); } - } catch (Throwable e) { - LogUtil.logThrowable(fixSession.getLog(), "Exception during ConnectTask run", e); + } catch (Exception e) { + LogUtil.logThrowable(fixSession.getLog(), ErrorEventReasons.IO_ERROR, "Exception during ConnectTask run", e); } } @@ -229,7 +224,7 @@ private void connect() { connectFuture = ioConnector.connect(nextSocketAddress, localAddress); } pollConnectFuture(); - } catch (Throwable e) { + } catch (Exception e) { handleConnectException(e); } } @@ -249,7 +244,7 @@ private void pollConnectFuture() { + (System.currentTimeMillis() - lastReconnectAttemptTime) + " ms."); } - } catch (Throwable e) { + } catch (Exception e) { handleConnectException(e); } } @@ -263,9 +258,10 @@ private void handleConnectException(Throwable e) { final String nextRetryMsg = " (Next retry in " + computeNextRetryConnectDelay() + " milliseconds)"; ConnectException wrappedException = new ConnectException(e, socketAddress); if (e instanceof IOException) { - fixSession.getLog().onErrorEvent(e.getClass().getName() + " during connection to " + socketAddress + ": " + e + nextRetryMsg); + fixSession.getLog().onErrorEvent(ErrorEventReasons.CONNECTION_FAILED, + e.getClass().getName() + " during connection to " + socketAddress + ": " + e + nextRetryMsg); } else { - LogUtil.logThrowable(fixSession.getLog(), "Exception during connection to " + socketAddress + nextRetryMsg, e); + LogUtil.logThrowable(fixSession.getLog(), ErrorEventReasons.IO_ERROR, "Exception during connection to " + socketAddress + nextRetryMsg, e); } fixSession.getStateListener().onConnectException(fixSession.getSessionID(), wrappedException); connectFuture = null; @@ -343,8 +339,8 @@ private void resetIoConnector() { ioSession.closeNow(); } ioSession = null; - } catch (Throwable e) { - LogUtil.logThrowable(fixSession.getLog(), "Exception during resetIoConnector call", e); + } catch (Exception e) { + LogUtil.logThrowable(fixSession.getLog(), ErrorEventReasons.IO_ERROR, "Exception during resetIoConnector call", e); } } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/message/FIXMessageDecoder.java b/quickfixj-core/src/main/java/quickfix/mina/message/FIXMessageDecoder.java index 815b8b895d..252371724f 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/message/FIXMessageDecoder.java +++ b/quickfixj-core/src/main/java/quickfix/mina/message/FIXMessageDecoder.java @@ -229,7 +229,7 @@ private boolean parseMessage(IoBuffer in, ProtocolDecoderOutput out) } } return messageFound; - } catch (Throwable t) { + } catch (Exception t) { resetState(); if (t instanceof ProtocolCodecException) { throw (ProtocolCodecException) t; diff --git a/quickfixj-core/src/test/java/quickfix/ApplicationFunctionalAdapterTest.java b/quickfixj-core/src/test/java/quickfix/ApplicationFunctionalAdapterTest.java index 50d6c8ed06..cbd7e419d2 100644 --- a/quickfixj-core/src/test/java/quickfix/ApplicationFunctionalAdapterTest.java +++ b/quickfixj-core/src/test/java/quickfix/ApplicationFunctionalAdapterTest.java @@ -122,8 +122,8 @@ public void testRemovedOnLogoutListenersNotInvoked() { @Test public void testToAdminListenersInvokedInOrder() { ApplicationFunctionalAdapter adapter = new ApplicationFunctionalAdapter(); - BiConsumer listener = mock(BiConsumer.class); - BiConsumer listener2 = mock(BiConsumer.class); + BiConsumer listener = mock(BiConsumer.class); + BiConsumer listener2 = mock(BiConsumer.class); adapter.addToAdminListener(listener); adapter.addToAdminListener(listener2); @@ -141,8 +141,8 @@ public void testToAdminListenersInvokedInOrder() { @Test public void testRemovedToAdminListenersNotInvoked() { ApplicationFunctionalAdapter adapter = new ApplicationFunctionalAdapter(); - BiConsumer listener = mock(BiConsumer.class); - BiConsumer listener2 = mock(BiConsumer.class); + BiConsumer listener = mock(BiConsumer.class); + BiConsumer listener2 = mock(BiConsumer.class); adapter.addToAdminListener(listener); adapter.addToAdminListener(listener2); diff --git a/quickfixj-core/src/test/java/quickfix/CompositeLogTest.java b/quickfixj-core/src/test/java/quickfix/CompositeLogTest.java index edf96cd7e2..205ae33cde 100644 --- a/quickfixj-core/src/test/java/quickfix/CompositeLogTest.java +++ b/quickfixj-core/src/test/java/quickfix/CompositeLogTest.java @@ -20,10 +20,26 @@ package quickfix; import static org.mockito.Mockito.*; -import junit.framework.TestCase; +import org.junit.Test; -public class CompositeLogTest extends TestCase { - public void testCompositeLog() throws Exception { +public class CompositeLogTest { + + @Test + public void testClearDelegates() { + Log mockLog1 = mock(Log.class); + Log mockLog2 = mock(Log.class); + + CompositeLog log = new CompositeLog(new Log[] { mockLog1, mockLog2 }); + log.setRethrowExceptions(true); + + log.clear(); + + verify(mockLog1).clear(); + verify(mockLog2).clear(); + } + + @Test + public void testOnIncomingDelegates() { Log mockLog1 = mock(Log.class); Log mockLog2 = mock(Log.class); @@ -31,14 +47,91 @@ public void testCompositeLog() throws Exception { log.setRethrowExceptions(true); log.onIncoming("INCOMING"); - log.onOutgoing("OUTGOING"); - log.onEvent("EVENT"); verify(mockLog1).onIncoming("INCOMING"); verify(mockLog2).onIncoming("INCOMING"); + } + + @Test + public void testOnOutgoingDelegates() { + Log mockLog1 = mock(Log.class); + Log mockLog2 = mock(Log.class); + + CompositeLog log = new CompositeLog(new Log[] { mockLog1, mockLog2 }); + log.setRethrowExceptions(true); + + log.onOutgoing("OUTGOING"); + verify(mockLog1).onOutgoing("OUTGOING"); verify(mockLog2).onOutgoing("OUTGOING"); + } + + @Test + public void testOnEventDelegates() { + Log mockLog1 = mock(Log.class); + Log mockLog2 = mock(Log.class); + + CompositeLog log = new CompositeLog(new Log[] { mockLog1, mockLog2 }); + log.setRethrowExceptions(true); + + log.onEvent("EVENT"); + verify(mockLog1).onEvent("EVENT"); verify(mockLog2).onEvent("EVENT"); } + + @Test + public void testOnErrorEventDelegates() { + Log mockLog1 = mock(Log.class); + Log mockLog2 = mock(Log.class); + + CompositeLog log = new CompositeLog(new Log[] { mockLog1, mockLog2 }); + log.setRethrowExceptions(true); + + log.onErrorEvent("Category","ERROR EVENT"); + + verify(mockLog1).onErrorEvent("Category","ERROR EVENT"); + verify(mockLog2).onErrorEvent("Category","ERROR EVENT"); + } + + @Test + public void testOnInvalidMessageDelegates() { + Log mockLog1 = mock(Log.class); + Log mockLog2 = mock(Log.class); + + CompositeLog log = new CompositeLog(new Log[] { mockLog1, mockLog2 }); + log.setRethrowExceptions(true); + + log.onInvalidMessage("INVALID MESSAGE", "FAILURE REASON"); + + verify(mockLog1).onInvalidMessage("INVALID MESSAGE", "FAILURE REASON"); + verify(mockLog2).onInvalidMessage("INVALID MESSAGE", "FAILURE REASON"); + } + + @Test(expected = OutOfMemoryError.class) + public void testOOMEIsThrownIfThrownByAComponentLogWhenRethrowExceptionsIsTrue() throws OutOfMemoryError { + Log mockLog1 = mock(Log.class); + Log mockLog2 = mock(Log.class); + + doThrow(new OutOfMemoryError()).when(mockLog1).onIncoming(anyString()); + + CompositeLog log = new CompositeLog(new Log[] { mockLog1, mockLog2 }); + log.setRethrowExceptions(true); + + log.onIncoming("INCOMING"); + } + + @Test(expected = OutOfMemoryError.class) + public void testOOMEIsThrownIfThrownByAComponentLogWhenRethrowExceptionsIsFalse() throws OutOfMemoryError { + Log mockLog1 = mock(Log.class); + Log mockLog2 = mock(Log.class); + + doThrow(new OutOfMemoryError()).when(mockLog1).onIncoming(anyString()); + + CompositeLog log = new CompositeLog(new Log[] { mockLog1, mockLog2 }); + log.setRethrowExceptions(false); + + log.onIncoming("INCOMING"); + } + } diff --git a/quickfixj-core/src/test/java/quickfix/DataDictionaryProxyTest.java b/quickfixj-core/src/test/java/quickfix/DataDictionaryProxyTest.java new file mode 100644 index 0000000000..73ace4bd07 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/DataDictionaryProxyTest.java @@ -0,0 +1,56 @@ +package quickfix; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.flextrade.jfixture.JFixture; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mockingDetails; +import static org.mockito.Mockito.reset; + +public class DataDictionaryProxyTest { + + @Mock + private DataDictionary mockDataDictionary; + private DataDictionaryProxy dataDictionaryProxy; + private JFixture fixture; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + fixture = new JFixture(); + } + + @Test + public void checkAllMethodsAreOverridden() + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, ConfigError { + dataDictionaryProxy = new DataDictionaryProxy(mockDataDictionary); + + for(Method m : DataDictionary.class.getMethods()) { + if (m.getDeclaringClass().equals(DataDictionary.class)) { + Object[] args = new Object[m.getParameterCount()]; + for (int i = 0; i < m.getParameterCount(); i++) { + if (m.getParameters()[i].getType().equals(Message.class)) { + args[i] = new Message(); + } else if (m.getParameters()[i].getType().equals(FieldMap.class)) { + args[i] = new Group(37, 11); + } else if (m.getParameters()[i].getType().equals(Field.class)) { + args[i] = new StringField(1); + } else { + args[i] = fixture.create(m.getParameters()[i].getType()); + } + } + m.invoke(dataDictionaryProxy, args); + assertEquals("Method: "+ m.getName()+" not defined", 1, mockingDetails(mockDataDictionary).getInvocations().size()); + assertEquals("Method: "+ m.getName()+" not invoked", m, mockingDetails(mockDataDictionary).getInvocations().iterator().next().getMethod()); + reset(mockDataDictionary); + } + } + } + +} diff --git a/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java b/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java index e660f5d8bc..ee169676c1 100644 --- a/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java +++ b/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java @@ -89,10 +89,15 @@ public void testDictionary() throws Exception { DataDictionary dd = getDictionary(); assertEquals("wrong field name", "Currency", dd.getFieldName(15)); + assertEquals("wrong field tag", 15, dd.getFieldTag("Currency")); assertEquals("wrong value description", "BUY", dd.getValueName(4, "B")); assertEquals("wrong value for given value name", "2", dd.getValue(54, "SELL")); assertEquals("wrong value type", FieldType.STRING, dd.getFieldType(1)); assertEquals("wrong version", FixVersions.BEGINSTRING_FIX44, dd.getVersion()); + assertEquals("incorrectly validates values", false, dd.isFieldValue(15, "10")); + assertEquals("incorrectly validates valid value", true, dd.isFieldValue(4, "B")); + assertEquals("incorrectly validates invalid value", false, dd.isFieldValue(4, "C")); + assertEquals("incorrectly validates multiple values", true, dd.isFieldValue(277, "A K")); assertFalse("unexpected field values existence", dd.hasFieldValue(1)); assertTrue("unexpected field values nonexistence", dd.hasFieldValue(4)); assertFalse("unexpected field existence", dd.isField(9999)); @@ -122,6 +127,77 @@ public void testDictionary() throws Exception { assertFalse(dd.isMsgField("UNKNOWN_TYPE", 1)); } + @Test + public void testGetOrderedRequiredFieldsForMessage() throws Exception { + DataDictionary dictionary = getDictionary(); + assertArrayEquals("incorrect field ordering", new int[]{8, 9, 35, 49, 56, 34, 52, 98, 108, 10}, + dictionary.getOrderedRequiredFieldsForMessage("A")); + } + + @Test + public void testGetOrderedFieldsForMessage() throws Exception { + DataDictionary dictionary = getDictionary(); + assertArrayEquals("incorrect field ordering", + new int[]{8, 9, 35, 49, 56, 115, 128, 90, 91, 34, 50, 142, 57, 143, 116, 144, 129, 145, + 43, 97, 52, 122, 212, 213, 347, 369, 627, 98, 108, 95, 96, 141, 789, 383, 384, + 464, 553, 554, 93, 89, 10}, + dictionary.getOrderedFieldsForMessage("A")); + } + + @Test + public void testNoFieldsWithStrictParsing() throws Exception { + String data = ""; + data += ""; + data += "

"; + data += " "; + data += "
"; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += ""; + + try { + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); + } catch (ConfigError e) { + // Expected + assertTrue(e.getMessage().contains("No fields found: msgType=")); + } + } + + @Test + public void testNoFieldsWithoutStrictParsing() throws Exception { + String data = ""; + data += ""; + data += "
"; + data += " "; + data += "
"; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
"; + + new DataDictionary(new ByteArrayInputStream(data.getBytes()), false); + } + + @Test public void testMissingFieldAttributeForRequired() throws Exception { String data = ""; @@ -149,7 +225,7 @@ public void testMissingFieldAttributeForRequired() throws Exception { private void assertConfigErrorForMissingAttributeRequired(String data) { try { - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } catch (ConfigError e) { // Expected assertTrue(e.getMessage().contains("does not have a 'required'")); @@ -181,6 +257,40 @@ public void testMissingComponentAttributeForRequired() throws Exception { assertConfigErrorForMissingAttributeRequired(data); } + @Test + public void testComponentGroupMissingRequiredAttribute() throws Exception { + String data = ""; + data += ""; + data += "
"; + data += " "; + data += "
"; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
"; + + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); + } + @Test public void testMissingGroupAttributeForRequired() throws Exception { String data = ""; @@ -232,7 +342,7 @@ public void testHeaderTrailerRequired() throws Exception { data += " "; data += ""; - DataDictionary dd = new DataDictionary(new ByteArrayInputStream(data.getBytes())); + DataDictionary dd = new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); assertEquals(1, dd.getNumMessageCategories()); assertEquals("0", dd.getMsgType("Heartbeat")); @@ -269,7 +379,7 @@ public void testMessageWithNoChildren40() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields found: msgType=msg"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -296,7 +406,7 @@ public void testMessageWithTextElement40() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields found: msgType=msg"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -320,7 +430,7 @@ public void testMessagesWithNoChildren40() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No messages defined"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -345,7 +455,7 @@ public void testMessagesWithTextElement40() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No messages defined"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -371,7 +481,7 @@ public void testHeaderWithNoChildren40() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields found: msgType=HEADER"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -398,7 +508,7 @@ public void testHeaderWithTextElement40() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields found: msgType=HEADER"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -424,7 +534,7 @@ public void testTrailerWithNoChildren40() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields found: msgType=TRAILER"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -451,7 +561,7 @@ public void testTrailerWithTextElement40() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields found: msgType=TRAILER"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -475,7 +585,7 @@ public void testFieldsWithNoChildren40() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields defined"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -500,7 +610,7 @@ public void testFieldsWithTextElement40() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields defined"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -526,7 +636,7 @@ public void testMessageWithNoChildren50() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields found: msgType=msg"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -553,7 +663,7 @@ public void testMessageWithTextElement50() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields found: msgType=msg"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -577,7 +687,7 @@ public void testMessagesWithNoChildren50() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No messages defined"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -602,7 +712,7 @@ public void testMessagesWithTextElement50() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No messages defined"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -625,7 +735,7 @@ public void testHeaderWithNoChildren50() throws Exception { data += " "; data += ""; - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -649,7 +759,7 @@ public void testHeaderWithTextElement50() throws Exception { data += " "; data += ""; - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -672,7 +782,7 @@ public void testTrailerWithNoChildren50() throws Exception { data += " "; data += ""; - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -696,7 +806,7 @@ public void testTrailerWithTextElement50() throws Exception { data += " "; data += ""; - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -716,7 +826,7 @@ public void testFieldsWithNoChildren50() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields defined"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -737,7 +847,7 @@ public void testFieldsWithTextElement50() throws Exception { expectedException.expect(ConfigError.class); expectedException.expectMessage("No fields defined"); - new DataDictionary(new ByteArrayInputStream(data.getBytes())); + new DataDictionary(new ByteArrayInputStream(data.getBytes()), true); } @Test @@ -763,11 +873,11 @@ public void testMessageValidateBodyOnly() throws Exception { new ExpectedTestFailure(FieldException.class, "field=") { @Override protected void execute() throws Throwable { - dd.validate(newSingle); + dd.validate(newSingle, new ValidationSettings()); } }.run(); - dd.validate(newSingle, true); + dd.validate(newSingle, true, new ValidationSettings()); } @Test @@ -786,13 +896,13 @@ public void testMessageDataDictionaryMismatch() throws Exception { "Message version 'FIX.4.3' does not match the data dictionary version 'FIX.4.4'") { @Override protected void execute() throws Throwable { - dd.validate(newSingle); + dd.validate(newSingle, new ValidationSettings()); } }.run(); // TODO: This is unexpected for pre-FIX 5.0 messages: // If bodyOnly is true, the correct data dictionary is not checked. - dd.validate(newSingle, true); + dd.validate(newSingle, true, new ValidationSettings()); } // QF C++ treats the string argument as a filename although it's @@ -800,7 +910,7 @@ protected void execute() throws Throwable { // ensures the DD works correctly with a regular file path. @Test public void testDictionaryWithFilename() throws Exception { - DataDictionary dd = new DataDictionary("FIX40.xml"); + DataDictionary dd = new DataDictionary("FIX40.xml", true); assertEquals("wrong field name", "Currency", dd.getFieldName(15)); // It worked! } @@ -814,7 +924,7 @@ public void testDictionaryInClassPath() throws Exception { ClassLoader previousContextClassLoader = currentThread.getContextClassLoader(); currentThread.setContextClassLoader(customClassLoader); try { - DataDictionary dd = new DataDictionary("FIX40.xml"); + DataDictionary dd = new DataDictionary("FIX40.xml", true); assertEquals("wrong field name", "Currency", dd.getFieldName(15)); // It worked! } finally { @@ -861,36 +971,38 @@ public void testAllowUnknownFields() throws Exception { newSingle.setField(new LastMkt("FOO")); final DataDictionary dictionary = new DataDictionary(getDictionary()); + final ValidationSettings validationSettings = new ValidationSettings(); new ExpectedTestFailure(FieldException.class, "field=") { @Override protected void execute() throws Throwable { - dictionary.validate(newSingle); + dictionary.validate(newSingle, validationSettings); } }.run(); - dictionary.setAllowUnknownMessageFields(true); - dictionary.validate(newSingle); + validationSettings.setAllowUnknownMessageFields(true); + dictionary.validate(newSingle, validationSettings); } // QFJ-535 @Test public void testValidateFieldsOutOfOrderForGroups() throws Exception { final DataDictionary dictionary = new DataDictionary(getDictionary()); - dictionary.setCheckUnorderedGroupFields(false); + final ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setCheckUnorderedGroupFields(false); Message messageWithGroupLevel1 = new Message( "8=FIX.4.4\0019=185\00135=D\00134=25\00149=SENDER\00156=TARGET\00152=20110412-13:43:00\001" + "60=20110412-13:43:00\0011=testAccount\00111=123\00121=3\00138=42\00140=2\00144=42.37\001" + "54=1\00155=QFJ\00159=0\00178=1\00179=allocAccount\001736=currency\001661=1\00110=130\001", - dictionary); - dictionary.validate(messageWithGroupLevel1); + dictionary, validationSettings); + dictionary.validate(messageWithGroupLevel1, validationSettings); Message messageWithGroupLevel2 = new Message( "8=FIX.4.4\0019=185\00135=D\00134=25\00149=SENDER\00156=TARGET\00152=20110412-13:43:00\001" + "60=20110412-13:43:00\0011=testAccount\00111=123\00121=3\00138=42\00140=2\00144=42.37\001" + "54=1\00155=QFJ\00159=0\00178=1\00179=allocAccount\001539=1\001524=1\001538=1\001525=a\00110=145\001", - dictionary); - dictionary.validate(messageWithGroupLevel2); + dictionary, validationSettings); + dictionary.validate(messageWithGroupLevel2, validationSettings); } // QFJ-535 @@ -898,7 +1010,8 @@ public void testValidateFieldsOutOfOrderForGroups() throws Exception { public void testNewOrderSingleWithCorrectTag50() throws Exception { final DataDictionary dataDictionary = new DataDictionary(getDictionary()); - dataDictionary.setCheckFieldsOutOfOrder(true); + final ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setCheckFieldsOutOfOrder(true); String correctFixMessage = "8=FIX.4.4\0019=218\00135=D\00149=cust\00150=trader\001" + "56=FixGateway\00134=449\00152=20110420-09:17:40\00111=clordid\00154=1\00138=50\001" + @@ -908,28 +1021,28 @@ public void testNewOrderSingleWithCorrectTag50() throws Exception { // in any case, it must be validated as the message is correct //doValidation and checkFieldsOutOfOrder final NewOrderSingle nos1 = new NewOrderSingle(); - nos1.fromString(correctFixMessage, dataDictionary, true); - dataDictionary.validate(nos1); + nos1.fromString(correctFixMessage, dataDictionary, validationSettings, true); + dataDictionary.validate(nos1, validationSettings); assertTrue(nos1.getHeader().isSetField(new SenderSubID())); //doNotValidation and checkFieldsOutOfOrder final NewOrderSingle nos2 = new NewOrderSingle(); - nos2.fromString(correctFixMessage, dataDictionary, false); - dataDictionary.validate(nos2); + nos2.fromString(correctFixMessage, dataDictionary, validationSettings, false); + dataDictionary.validate(nos2, validationSettings); assertTrue(nos2.getHeader().isSetField(new SenderSubID())); - dataDictionary.setCheckFieldsOutOfOrder(false); + validationSettings.setCheckFieldsOutOfOrder(false); //doValidation and no checkFieldsOutOfOrder final NewOrderSingle nos3 = new NewOrderSingle(); - nos3.fromString(correctFixMessage, dataDictionary, true); - dataDictionary.validate(nos3); + nos3.fromString(correctFixMessage, dataDictionary, validationSettings, true); + dataDictionary.validate(nos3, validationSettings); assertTrue(nos3.getHeader().isSetField(new SenderSubID())); //doNotValidation and no checkFieldsOutOfOrder final NewOrderSingle nos4 = new NewOrderSingle(); - nos4.fromString(correctFixMessage, dataDictionary, false); - dataDictionary.validate(nos4); + nos4.fromString(correctFixMessage, dataDictionary, validationSettings, false); + dataDictionary.validate(nos4, validationSettings); assertTrue(nos4.getHeader().isSetField(new SenderSubID())); } @@ -937,7 +1050,8 @@ public void testNewOrderSingleWithCorrectTag50() throws Exception { public void testNewOrderSingleWithMisplacedTag50() throws Exception { final DataDictionary dataDictionary = new DataDictionary(getDictionary()); - dataDictionary.setCheckFieldsOutOfOrder(true); + final ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setCheckFieldsOutOfOrder(true); String incorrectFixMessage = "8=FIX.4.4\0019=218\00135=D\00149=cust\00156=FixGateway\001" + "34=449\00152=20110420-09:17:40\00111=clordid\00154=1\00138=50\00159=6\00140=2\001" + @@ -947,76 +1061,32 @@ public void testNewOrderSingleWithMisplacedTag50() throws Exception { //doValidation and checkFieldsOutOfOrder -> should fail final NewOrderSingle nos1 = new NewOrderSingle(); try { - nos1.fromString(incorrectFixMessage, dataDictionary, true); + nos1.fromString(incorrectFixMessage, dataDictionary, validationSettings, true); } catch (FieldException fe) { // expected exception } //doNotValidation and checkFieldsOutOfOrder -> should NOT fail final NewOrderSingle nos2 = new NewOrderSingle(); - nos2.fromString(incorrectFixMessage, dataDictionary, false); - dataDictionary.validate(nos2); + nos2.fromString(incorrectFixMessage, dataDictionary, validationSettings, false); + dataDictionary.validate(nos2, validationSettings); assertTrue(nos2.getHeader().isSetField(new SenderSubID())); - dataDictionary.setCheckFieldsOutOfOrder(false); + validationSettings.setCheckFieldsOutOfOrder(false); //doValidation and no checkFieldsOutOfOrder -> should NOT fail final NewOrderSingle nos3 = new NewOrderSingle(); - nos3.fromString(incorrectFixMessage, dataDictionary, true); - dataDictionary.validate(nos3); + nos3.fromString(incorrectFixMessage, dataDictionary, validationSettings, true); + dataDictionary.validate(nos3, validationSettings); assertTrue(nos3.getHeader().isSetField(new SenderSubID())); //doNotValidation and no checkFieldsOutOfOrder -> should NOT fail final NewOrderSingle nos4 = new NewOrderSingle(); - nos4.fromString(incorrectFixMessage, dataDictionary, false); - dataDictionary.validate(nos4); + nos4.fromString(incorrectFixMessage, dataDictionary, validationSettings, false); + dataDictionary.validate(nos4, validationSettings); assertTrue(nos4.getHeader().isSetField(new SenderSubID())); } - @Test - public void testCopy() throws Exception { - final DataDictionary dataDictionary = new DataDictionary(getDictionary()); - - dataDictionary.setAllowUnknownMessageFields(true); - dataDictionary.setCheckFieldsHaveValues(false); - dataDictionary.setCheckFieldsOutOfOrder(false); - dataDictionary.setCheckUnorderedGroupFields(false); - dataDictionary.setCheckUserDefinedFields(false); - - DataDictionary ddCopy = new DataDictionary(dataDictionary); - - assertEquals(ddCopy.isAllowUnknownMessageFields(),dataDictionary.isAllowUnknownMessageFields()); - assertEquals(ddCopy.isCheckFieldsHaveValues(),dataDictionary.isCheckFieldsHaveValues()); - assertEquals(ddCopy.isCheckFieldsOutOfOrder(),dataDictionary.isCheckFieldsOutOfOrder()); - assertEquals(ddCopy.isCheckUnorderedGroupFields(),dataDictionary.isCheckUnorderedGroupFields()); - assertEquals(ddCopy.isCheckUserDefinedFields(),dataDictionary.isCheckUserDefinedFields()); - assertArrayEquals(getDictionary().getOrderedFields(),ddCopy.getOrderedFields()); - assertArrayEquals(getDictionary().getOrderedFields(),dataDictionary.getOrderedFields()); - - DataDictionary.GroupInfo groupFromDDCopy = ddCopy.getGroup(NewOrderSingle.MSGTYPE, NoPartyIDs.FIELD); - assertTrue(groupFromDDCopy.getDataDictionary().isAllowUnknownMessageFields()); - // set to false on ORIGINAL DD - dataDictionary.setAllowUnknownMessageFields(false); - assertFalse(dataDictionary.isAllowUnknownMessageFields()); - assertFalse(dataDictionary.getGroup(NewOrderSingle.MSGTYPE, NoPartyIDs.FIELD).getDataDictionary().isAllowUnknownMessageFields()); - // should be still true on COPIED DD and its group - assertTrue(ddCopy.isAllowUnknownMessageFields()); - groupFromDDCopy = ddCopy.getGroup(NewOrderSingle.MSGTYPE, NoPartyIDs.FIELD); - assertTrue(groupFromDDCopy.getDataDictionary().isAllowUnknownMessageFields()); - - DataDictionary originalGroupDictionary = getDictionary().getGroup(NewOrderSingle.MSGTYPE, NoPartyIDs.FIELD).getDataDictionary(); - DataDictionary groupDictionary = dataDictionary.getGroup(NewOrderSingle.MSGTYPE, NoPartyIDs.FIELD).getDataDictionary(); - DataDictionary copyGroupDictionary = ddCopy.getGroup(NewOrderSingle.MSGTYPE, NoPartyIDs.FIELD).getDataDictionary(); - assertArrayEquals(originalGroupDictionary.getOrderedFields(), groupDictionary.getOrderedFields()); - assertArrayEquals(originalGroupDictionary.getOrderedFields(), copyGroupDictionary.getOrderedFields()); - - DataDictionary originalNestedGroupDictionary = originalGroupDictionary.getGroup(NewOrderSingle.MSGTYPE, NoPartySubIDs.FIELD).getDataDictionary(); - DataDictionary nestedGroupDictionary = groupDictionary.getGroup(NewOrderSingle.MSGTYPE, NoPartySubIDs.FIELD).getDataDictionary(); - DataDictionary copyNestedGroupDictionary = copyGroupDictionary.getGroup(NewOrderSingle.MSGTYPE, NoPartySubIDs.FIELD).getDataDictionary(); - assertArrayEquals(originalNestedGroupDictionary.getOrderedFields(), nestedGroupDictionary.getOrderedFields()); - assertArrayEquals(originalNestedGroupDictionary.getOrderedFields(), copyNestedGroupDictionary.getOrderedFields()); - } - @Test public void testOrderedFields() throws Exception { final DataDictionary dataDictionary = getDictionary(); @@ -1047,13 +1117,14 @@ public void testNonUDFDefinedInFieldsSectionDontAllowUMFDontCheckUDF() throws Ex Message quoteRequest = createQuoteRequest(); quoteRequest.setDecimal(AvgPx.FIELD, new BigDecimal(1.2345)); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(false); - dataDictionary.setCheckUserDefinedFields(false); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(false); + validationSettings.setCheckUserDefinedFields(false); expectedException.expect(FieldException.class); expectedException.expectMessage("Tag not defined for this message type, field=6"); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1061,13 +1132,14 @@ public void testNonUDFDefinedInFieldsSectionDontAllowUMFCheckUDF() throws Except Message quoteRequest = createQuoteRequest(); quoteRequest.setDecimal(AvgPx.FIELD, new BigDecimal(1.2345)); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(false); - dataDictionary.setCheckUserDefinedFields(true); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(false); + validationSettings.setCheckUserDefinedFields(true); expectedException.expect(FieldException.class); expectedException.expectMessage("Tag not defined for this message type, field=6"); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1075,11 +1147,12 @@ public void testNonUDFDefinedInFieldsSectionAllowUMFDontCheckUDF() throws Except Message quoteRequest = createQuoteRequest(); quoteRequest.setDecimal(AvgPx.FIELD, new BigDecimal(1.2345)); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(true); - dataDictionary.setCheckUserDefinedFields(false); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(true); + validationSettings.setCheckUserDefinedFields(false); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1087,11 +1160,12 @@ public void testNonUDFDefinedInFieldsSectionAllowUMFCheckUDF() throws Exception Message quoteRequest = createQuoteRequest(); quoteRequest.setDecimal(AvgPx.FIELD, new BigDecimal(1.2345)); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(true); - dataDictionary.setCheckUserDefinedFields(true); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(true); + validationSettings.setCheckUserDefinedFields(true); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } /** @@ -1111,11 +1185,12 @@ public void testUDFDefinedInFieldsSectionDontAllowUMFDontCheckUDF() throws Excep Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(5000, 555); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(false); - dataDictionary.setCheckUserDefinedFields(false); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(false); + validationSettings.setCheckUserDefinedFields(false); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1123,13 +1198,14 @@ public void testUDFDefinedInFieldsSectionDontAllowUMFCheckUDF() throws Exception Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(5000, 555); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(false); - dataDictionary.setCheckUserDefinedFields(true); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(false); + validationSettings.setCheckUserDefinedFields(true); expectedException.expect(FieldException.class); expectedException.expectMessage("Tag not defined for this message type, field=5000"); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1137,11 +1213,12 @@ public void testUDFDefinedInFieldsSectionAllowUMFDontCheckUDF() throws Exception Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(5000, 555); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(true); - dataDictionary.setCheckUserDefinedFields(false); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(true); + validationSettings.setCheckUserDefinedFields(false); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1149,13 +1226,14 @@ public void testUDFDefinedInFieldsSectionAllowUMFCheckUDF() throws Exception { Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(5000, 555); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(true); - dataDictionary.setCheckUserDefinedFields(true); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(true); + validationSettings.setCheckUserDefinedFields(true); expectedException.expect(FieldException.class); expectedException.expectMessage("Tag not defined for this message type, field=5000"); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } /** @@ -1175,13 +1253,14 @@ public void testNonUDFNotDefinedInFieldsSectionDontAllowUMFDontCheckUDF() throws Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(1000, 111); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(false); - dataDictionary.setCheckUserDefinedFields(false); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(false); + validationSettings.setCheckUserDefinedFields(false); expectedException.expect(FieldException.class); expectedException.expectMessage("Invalid tag number, field=1000"); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1189,13 +1268,14 @@ public void testNonUDFNotDefinedInFieldsSectionDontAllowUMFCheckUDF() throws Exc Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(1000, 111); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(false); - dataDictionary.setCheckUserDefinedFields(true); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(false); + validationSettings.setCheckUserDefinedFields(true); expectedException.expect(FieldException.class); expectedException.expectMessage("Invalid tag number, field=1000"); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1203,11 +1283,12 @@ public void testNonUDFNotDefinedInFieldsSectionAllowUMFDontCheckUDF() throws Exc Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(1000, 111); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(true); - dataDictionary.setCheckUserDefinedFields(false); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(true); + validationSettings.setCheckUserDefinedFields(false); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1215,11 +1296,12 @@ public void testNonUDFNotDefinedInFieldsSectionAllowUMFCheckUDF() throws Excepti Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(1000, 111); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(true); - dataDictionary.setCheckUserDefinedFields(true); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(true); + validationSettings.setCheckUserDefinedFields(true); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } /** @@ -1239,11 +1321,12 @@ public void testUDFNotDefinedInFieldsSectionDontAllowUMFDontCheckUDF() throws Ex Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(6000, 666); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(false); - dataDictionary.setCheckUserDefinedFields(false); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(false); + validationSettings.setCheckUserDefinedFields(false); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1251,13 +1334,14 @@ public void testUDFNotDefinedInFieldsSectionDontAllowUMFCheckUDF() throws Except Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(6000, 666); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(false); - dataDictionary.setCheckUserDefinedFields(true); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(false); + validationSettings.setCheckUserDefinedFields(true); expectedException.expect(FieldException.class); expectedException.expectMessage("Invalid tag number, field=6000"); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1265,11 +1349,12 @@ public void testUDFNotDefinedInFieldsSectionAllowUMFDontCheckUDF() throws Except Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(6000, 666); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(true); - dataDictionary.setCheckUserDefinedFields(false); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(true); + validationSettings.setCheckUserDefinedFields(false); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1277,13 +1362,14 @@ public void testUDFNotDefinedInFieldsSectionAllowUMFCheckUDF() throws Exception Message quoteRequest = createQuoteRequest(); quoteRequest.setInt(6000, 666); - DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setAllowUnknownMessageFields(true); - dataDictionary.setCheckUserDefinedFields(true); + DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(true); + validationSettings.setCheckUserDefinedFields(true); expectedException.expect(FieldException.class); expectedException.expectMessage("Invalid tag number, field=6000"); - dataDictionary.validate(quoteRequest, true); + dataDictionary.validate(quoteRequest, true, validationSettings); } private Message createQuoteRequest() { @@ -1312,12 +1398,13 @@ public void testGroupWithReqdComponentWithReqdFieldValidation() throws Exception final Message quoteRequest = createQuoteRequest(); quoteRequest.getGroup(1, NoRelatedSym.FIELD).removeField(Symbol.FIELD); final DataDictionary dictionary = getDictionary(); + final ValidationSettings validationSettings = new ValidationSettings(); expectedException.expect(FieldException.class); expectedException.expect(hasProperty("sessionRejectReason", is(SessionRejectReason.REQUIRED_TAG_MISSING))); expectedException.expect(hasProperty("field", is(Symbol.FIELD))); - dictionary.validate(quoteRequest, true); + dictionary.validate(quoteRequest, true, validationSettings); } @Test @@ -1339,7 +1426,8 @@ public void testRequiredFieldInsideComponentWithinRepeatingGroup() throws Except @Test public void testAllowingBlankValuesDisablesFieldValidation() throws Exception { final DataDictionary dictionary = getDictionary(); - dictionary.setCheckFieldsHaveValues(false); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setCheckFieldsHaveValues(false); final quickfix.fix44.NewOrderSingle newSingle = new quickfix.fix44.NewOrderSingle( new ClOrdID("123"), new Side(Side.BUY), new TransactTime(), new OrdType(OrdType.LIMIT) ); @@ -1351,7 +1439,7 @@ public void testAllowingBlankValuesDisablesFieldValidation() throws Exception { newSingle.setField(new TimeInForce(TimeInForce.DAY)); newSingle.setField(new Account("testAccount")); newSingle.setField(new StringField(EffectiveTime.FIELD)); - dictionary.validate(newSingle, true); + dictionary.validate(newSingle, true, validationSettings); } @@ -1368,7 +1456,7 @@ public void testConcurrentValidationFailure() throws Exception { final int noOfIterations = 500; for (int i = 0; i < noOfIterations; i++) { - final DataDictionary dd = new DataDictionary("FIX44.xml"); + final DataDictionary dd = new DataDictionary("FIX44.xml", true); final MessageFactory messageFactory = new quickfix.fix44.MessageFactory(); PausableThreadPoolExecutor ptpe = new PausableThreadPoolExecutor(noOfThreads); // submit threads to pausable executor and try to let them start at the same time @@ -1376,7 +1464,7 @@ public void testConcurrentValidationFailure() throws Exception { List resultList = new ArrayList<>(); for (int j = 0; j < noOfThreads; j++) { final Callable messageParser = (Callable) () -> { - Message msg = MessageUtils.parse(messageFactory, dd, msgString); + Message msg = MessageUtils.parse(messageFactory, dd, new ValidationSettings(), msgString); Group partyGroup = msg.getGroups(quickfix.field.NoPartyIDs.FIELD).get(0); char partyIdSource = partyGroup.getChar(PartyIDSource.FIELD); assertEquals(PartyIDSource.PROPRIETARY_CUSTOM_CODE, partyIdSource); @@ -1527,6 +1615,6 @@ public static DataDictionary getDictionary() throws Exception { */ public static DataDictionary getDictionary(String fileName) throws Exception { return new DataDictionary(DataDictionaryTest.class.getClassLoader() - .getResourceAsStream(fileName)); + .getResourceAsStream(fileName), true); } } diff --git a/quickfixj-core/src/test/java/quickfix/DefaultDataDictionaryProviderTest.java b/quickfixj-core/src/test/java/quickfix/DefaultDataDictionaryProviderTest.java index 39051326b9..cd8e56adac 100644 --- a/quickfixj-core/src/test/java/quickfix/DefaultDataDictionaryProviderTest.java +++ b/quickfixj-core/src/test/java/quickfix/DefaultDataDictionaryProviderTest.java @@ -39,7 +39,7 @@ public class DefaultDataDictionaryProviderTest { @BeforeClass public static void setUp() throws Exception { - dictionaryForTest1 = new DataDictionary("FIX44_Custom_Test.xml"); + dictionaryForTest1 = new DataDictionary("FIX44_Custom_Test.xml", true); dictionaryForTest2 = new DataDictionary(dictionaryForTest1); } @@ -86,7 +86,7 @@ public void throwExceptionIfSessionDictionaryIsNotFound() throws Exception { } @Test - public void returnRegisteredAppDictionaryWithoutDiscovery() throws Exception { + public void returnRegisteredAppDictionaryWithoutDiscovery() throws Exception { DefaultDataDictionaryProvider provider = new DefaultDataDictionaryProvider(false); provider.addApplicationDictionary(new ApplVerID(FIX44), dictionaryForTest1); provider.addApplicationDictionary(new ApplVerID(FIX40), dictionaryForTest2); diff --git a/quickfixj-core/src/test/java/quickfix/DefaultSessionFactoryTest.java b/quickfixj-core/src/test/java/quickfix/DefaultSessionFactoryTest.java index 3b8069061f..d36f5641ea 100644 --- a/quickfixj-core/src/test/java/quickfix/DefaultSessionFactoryTest.java +++ b/quickfixj-core/src/test/java/quickfix/DefaultSessionFactoryTest.java @@ -36,7 +36,9 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.isNull; public class DefaultSessionFactoryTest { @@ -97,6 +99,8 @@ public void testFixtDataDictionaryConfiguration() throws Exception { settings.setString(sessionID, Session.SETTING_DEFAULT_APPL_VER_ID, "FIX.4.2"); settings.setString(sessionID, Session.SETTING_APP_DATA_DICTIONARY, "FIX42.xml"); settings.setString(sessionID, Session.SETTING_APP_DATA_DICTIONARY + "." + FixVersions.BEGINSTRING_FIX40, "FIX40.xml"); + settings.setString(sessionID, Session.SETTING_ALLOW_UNKNOWN_MSG_FIELDS, "Y"); + settings.setString(sessionID, Session.SETTING_VALIDATE_UNORDERED_GROUP_FIELDS, "N"); try (Session session = factory.create(sessionID, settings)) { @@ -108,6 +112,56 @@ public void testFixtDataDictionaryConfiguration() throws Exception { is(notNullValue())); assertThat(provider.getApplicationDataDictionary(new ApplVerID(ApplVerID.FIX40)), is(notNullValue())); + assertTrue(session.getValidationSettings().isAllowUnknownMessageFields()); + assertFalse(session.getValidationSettings().isCheckUnorderedGroupFields()); + } + } + + @Test + public void testFixtWithFixDataDictionaryConfiguration() { + SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIXT11, "SENDER", "TARGET"); + setUpDefaultSettings(sessionID); + settings.setBool(sessionID, Session.SETTING_USE_DATA_DICTIONARY, true); + settings.setString(sessionID, Session.SETTING_TRANSPORT_DATA_DICTIONARY, "FIXT11.xml"); + settings.setString(sessionID, Session.SETTING_DEFAULT_APPL_VER_ID, "FIX.4.2"); + settings.setString(sessionID, Session.SETTING_DATA_DICTIONARY, "FIX42.xml"); + settings.setString(sessionID, Session.SETTING_ALLOW_UNKNOWN_MSG_FIELDS, "Y"); + settings.setString(sessionID, Session.SETTING_VALIDATE_UNORDERED_GROUP_FIELDS, "N"); + + try { + factory.create(sessionID, settings); + fail("Expected Config Error"); + } catch (ConfigError e) { + assertEquals("DataDictionary setting not supported in FIXT sessions", e.getMessage()); + } + } + + /** + * Tests that validation settings are applied when no explicit AppDataDictionary is defined. + * QFJ-981 + */ + @Test + public void testFixtDataDictionaryConfigurationWithDefaultAppDataDictionary() throws Exception { + SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIXT11, "SENDER", "TARGET"); + setUpDefaultSettings(sessionID); + settings.setBool(sessionID, Session.SETTING_USE_DATA_DICTIONARY, true); + settings.setString(sessionID, Session.SETTING_TRANSPORT_DATA_DICTIONARY, "FIXT11.xml"); + settings.setString(sessionID, Session.SETTING_DEFAULT_APPL_VER_ID, "FIX.4.2"); + settings.setString(sessionID, Session.SETTING_ALLOW_UNKNOWN_MSG_FIELDS, "Y"); + settings.setString(sessionID, Session.SETTING_VALIDATE_UNORDERED_GROUP_FIELDS, "N"); + + try (Session session = factory.create(sessionID, settings)) { + + DataDictionaryProvider provider = session.getDataDictionaryProvider(); + assertThat(provider.getSessionDataDictionary(sessionID.getBeginString()), + is(notNullValue())); + + assertThat(provider.getApplicationDataDictionary(new ApplVerID(ApplVerID.FIX42)), + is(notNullValue())); + assertThat(provider.getApplicationDataDictionary(new ApplVerID(ApplVerID.FIX40)), + is(notNullValue())); + assertTrue(session.getValidationSettings().isAllowUnknownMessageFields()); + assertFalse(session.getValidationSettings().isCheckUnorderedGroupFields()); } } @@ -125,6 +179,13 @@ public void testPreFixtDataDictionaryConfiguration() throws Exception { } } + @Test + public void testUseDataDictionaryFalse_hasNoDictionaryProvider() throws Exception { + settings.setBool(sessionID, Session.SETTING_USE_DATA_DICTIONARY, false); + Session session = factory.create(sessionID, settings); + assertNull(session.getDataDictionaryProvider()); + } + @Test public void testNoConnectionType() throws Exception { settings.removeSetting(sessionID, SessionFactory.SETTING_CONNECTION_TYPE); @@ -268,4 +329,12 @@ public void testRejectGarbledMessageAndNotValidateChecksumError() { "garbled message and process messages with invalid checksum at the same time.*"); } + // FIXHUB-952 + @Test + public void testFlexTradeAdditions() throws Exception { + settings.setString("Y", Session.SETTING_ALLOW_POS_DUP_MESSAGES); + settings.setString("Y", Session.SETTING_FIX_41_RESEND_AS_FIX42); + Session session = factory.create(sessionID, settings); + } + } diff --git a/quickfixj-core/src/test/java/quickfix/DefaultSessionScheduleTest.java b/quickfixj-core/src/test/java/quickfix/DefaultSessionScheduleTest.java new file mode 100644 index 0000000000..779263a733 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/DefaultSessionScheduleTest.java @@ -0,0 +1,379 @@ +package quickfix; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import quickfix.field.TimeUnit; + +import java.io.ByteArrayInputStream; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +public class DefaultSessionScheduleTest { + + private SessionID sessionID; + @Mock + private SystemTimeSource mockTimeSource; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + + sessionID = new SessionID("FIX.4.2:A->B"); + SystemTime.setTimeSource(mockTimeSource); + } + + @After + public void after() { + SystemTime.setTimeSource(null); + } + + @Test + public void constructor_with_StartTime_and_no_EndTime() throws ConfigError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "StartTime=00:00:00\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + assertThrows(ConfigError.class, () -> new DefaultSessionSchedule(sessionSettings, sessionID)); + } + + @Test + public void constructor_with_EndTime_and_no_StartTime() throws ConfigError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "EndTime=00:00:00\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + assertThrows(ConfigError.class, () -> new DefaultSessionSchedule(sessionSettings, sessionID)); + } + + @Test + public void constructor_with_StartDay_and_no_EndDay() throws ConfigError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "StartTime=00:00:00\n" + + "EndTime=00:00:00\n" + + "StartDay=Monday\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + assertThrows(ConfigError.class, () -> new DefaultSessionSchedule(sessionSettings, sessionID)); + } + + @Test + public void constructor_with_EndDay_and_no_StartDay() throws ConfigError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "StartTime=00:00:00\n" + + "EndTime=00:00:00\n" + + "EndDay=Monday\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + assertThrows(ConfigError.class, () -> new DefaultSessionSchedule(sessionSettings, sessionID)); + } + + @Test + public void constructor_with_TimePeriods() throws ConfigError, FieldConvertError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "TimePeriods=Monday 00:00:00>Tuesday 00:00:00\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + new DefaultSessionSchedule(sessionSettings, sessionID); + } + + @Test + public void isNonStopSession_returns_true_when_SETTING_NON_STOP_SESSION_Y() throws FieldConvertError, ConfigError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=Y\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertTrue(schedule.isNonStopSession()); + } + + @Test + public void isNonStopSession_returns_false_when_SETTING_NON_STOP_SESSION_N() throws FieldConvertError, ConfigError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "StartTime=00:00:00\n" + + "EndTime=00:00:01\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertFalse(schedule.isNonStopSession()); + } + + @Test + public void isNonStopSession_returns_false_when_SETTING_NON_STOP_SESSION_not_present() throws FieldConvertError, ConfigError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "StartTime=00:00:00\n" + + "EndTime=00:00:01\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertFalse(schedule.isNonStopSession()); + } + + @Test + public void isSessionTime_returns_true_for_NON_STOP_session() throws FieldConvertError, ConfigError { + when(mockTimeSource.getTime()).thenReturn(1L); + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=Y\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertTrue(schedule.isSessionTime()); + } + + @Test + public void isSessionTime_returns_true_for_DAILY_session_for_time_within_window() throws FieldConvertError, ConfigError { + when(mockTimeSource.getTime()).thenReturn(1L); + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "StartTime=00:00:00\n" + + "EndTime=00:00:01\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertTrue(schedule.isSessionTime()); + } + + @Test + public void isSessionTime_returns_false_for_DAILY_session_for_time_outside_window() throws FieldConvertError, ConfigError { + when(mockTimeSource.getTime()).thenReturn(2000L); + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "StartTime=00:00:00\n" + + "EndTime=00:00:01\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertFalse(schedule.isSessionTime()); + } + + @Test + public void isSessionTime_returnYes_true_for_WEEKDAYS_session_for_time_inside_window() throws FieldConvertError, ConfigError { + when(mockTimeSource.getTime()).thenReturn(1658241033266L); + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "Weekdays=Tuesday\n" + + "StartTime=10:00:00\n" + + "EndTime=20:00:01\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertTrue(schedule.isSessionTime()); + } + + @Test + public void isSessionTime_returns_true_for_TIME_PERIODS_session_for_time_inside_window() throws FieldConvertError, ConfigError { + when(mockTimeSource.getTime()).thenReturn(1658241033266L); + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "TimePeriods=Tuesday 00:00:02>Wednesday 00:00:03\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertTrue(schedule.isSessionTime()); + } + + @Test + public void isSessionTime_returns_true_for_TIME_PERIODS_session_for_time_inside_second_window() throws FieldConvertError, ConfigError { + when(mockTimeSource.getTime()).thenReturn(1658241033266L); + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "TimePeriods=Monday 00:00:00>Tuesday 00:00:01,Tuesday 00:00:02>Wednesday 00:00:03\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertTrue(schedule.isSessionTime()); + } + + @Test + public void toString_with_NonStopSession() throws ConfigError, FieldConvertError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=Y\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertEquals("non-stop", schedule.toString()); + } + + @Test + public void toString_with_noStartDayOrWeekdays() throws ConfigError, FieldConvertError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "StartTime=00:00:00\n" + + "EndTime=00:00:01\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertEquals("daily: 00:00:00-UTC - 00:00:01-UTC", schedule.toString()); + } + + @Test + public void toString_with_StartDay() throws ConfigError, FieldConvertError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "StartDay=Monday\n" + + "EndDay=Friday\n" + + "StartTime=00:00:00\n" + + "EndTime=00:00:01\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertEquals("weekly: MON 00:00:00-UTC - FRI 00:00:01-UTC", schedule.toString()); + } + + @Test + public void toString_with_Weekdays() throws ConfigError, FieldConvertError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "Weekdays=Monday,Tuesday\n" + + "StartTime=00:00:00\n" + + "EndTime=00:00:01\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertEquals("weekdays: MON, TUE, 00:00:00-UTC - 00:00:01-UTC", schedule.toString()); + } + + @Test + public void toString_with_TimePeriods() throws ConfigError, FieldConvertError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "TimePeriods=Monday 00:00:00>Tuesday 00:00:01\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertEquals("periods: MON 00:00:00-UTC - TUE 00:00:01-UTC", schedule.toString()); + } + + @Test + public void toString_with_multipleTimePeriods() throws ConfigError, FieldConvertError { + String sessionSettingsString = "" + + "[DEFAULT]\n" + + "NonStopSession=N\n" + + "\n" + + "[SESSION]\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=A\n" + + "TargetCompID=B\n" + + "TimePeriods=Monday 00:00:00>Tuesday 00:00:01,Tuesday 00:00:02>Wednesday 00:00:03\n"; + SessionSettings sessionSettings = new SessionSettings(new ByteArrayInputStream(sessionSettingsString.getBytes())); + DefaultSessionSchedule schedule = new DefaultSessionSchedule(sessionSettings, sessionID); + + assertEquals("periods: MON 00:00:00-UTC - TUE 00:00:01-UTC, TUE 00:00:02-UTC - WED 00:00:03-UTC", schedule.toString()); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/FieldMapTest.java b/quickfixj-core/src/test/java/quickfix/FieldMapTest.java index f347e685ed..a8ccb3cf3a 100644 --- a/quickfixj-core/src/test/java/quickfix/FieldMapTest.java +++ b/quickfixj-core/src/test/java/quickfix/FieldMapTest.java @@ -17,14 +17,16 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -/** - * Tests the {@link FieldMap} class. - * Specifically, verifies that the setters for {@link UtcTimeStampField} work correctly. - * - * @author toli - */ public class FieldMapTest { + @Test + public void testDefaultFieldOrderIsInsertionOrdering() { + FieldMap map = new Message(); + map.setString(2, "A"); + map.setString(1, "B"); + assertEquals("9=82=A1=B10=017",map.toString()); + } + @Test public void testSetUtcTimeStampField() throws Exception { FieldMap map = new Message(); @@ -80,16 +82,16 @@ private void testOrdering(int[] vals, int[] order, int[] expected) { @Test public void testOrdering() { - testOrdering(new int[]{1, 2, 3}, null, new int[]{1, 2, 3}); - testOrdering(new int[]{3, 2, 1}, null, new int[]{1, 2, 3}); - testOrdering(new int[]{1, 2, 3}, new int[]{1, 2, 3}, new int[]{1, 2, 3}); - testOrdering(new int[]{3, 2, 1}, new int[]{1, 2, 3}, new int[]{1, 2, 3}); - testOrdering(new int[]{1, 2, 3}, new int[]{1, 3, 2}, new int[]{1, 3, 2}); - testOrdering(new int[]{3, 2, 1}, new int[]{1, 3, 2}, new int[]{1, 3, 2}); - testOrdering(new int[]{1, 2, 3}, new int[]{1, 3}, new int[]{1, 3, 2}); - testOrdering(new int[]{3, 2, 1}, new int[]{1, 3}, new int[]{1, 3, 2}); - testOrdering(new int[]{1, 2, 3}, new int[]{3, 1}, new int[]{3, 1, 2}); - testOrdering(new int[]{3, 2, 1}, new int[]{3, 1}, new int[]{3, 1, 2}); + testOrdering(new int[] { 1, 2, 3 }, null, new int[] { 1, 2, 3 }); + testOrdering(new int[] { 3, 2, 1 }, null, new int[] { 3, 2, 1 }); + testOrdering(new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 }); + testOrdering(new int[] { 3, 2, 1 }, new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 }); + testOrdering(new int[] { 1, 2, 3 }, new int[] { 1, 3, 2 }, new int[] { 1, 3, 2 }); + testOrdering(new int[] { 3, 2, 1 }, new int[] { 1, 3, 2 }, new int[] { 1, 3, 2 }); + testOrdering(new int[] { 1, 2, 3 }, new int[] { 1, 3 }, new int[] { 1, 3, 2 }); + testOrdering(new int[] { 3, 2, 1 }, new int[] { 1, 3 }, new int[] { 1, 3, 2 }); + testOrdering(new int[] { 1, 2, 3 }, new int[] { 3, 1 }, new int[] { 3, 1, 2 }); + testOrdering(new int[] { 3, 2, 1 }, new int[] { 3, 1 }, new int[] { 3, 1, 2 }); } @Test @@ -119,6 +121,50 @@ public void testNullFieldException() { assertThrows(FieldException.class, () -> map.setField(field)); } + public void testGroupRemovalByGroup() { + FieldMap map = new Message(); + Group g = new Group(73, 11); + map.addGroup(g); + assertEquals(1, map.getGroups(73).size()); + assertTrue(map.isSetField(73)); + + map.removeGroup(g); + + //Method doesn't remove group entry + assertTrue(map.getGroups().containsKey(73)); + assertEquals(0, map.getGroups(73).size()); + assertFalse(map.isSetField(73)); + } + + public void testGroupRemovalByField() { + FieldMap map = new Message(); + Group g = new Group(73, 11); + map.addGroup(g); + assertEquals(1, map.getGroups(73).size()); + assertTrue(map.isSetField(73)); + + map.removeGroup(73); + + //Method doesn't remove group entry + assertTrue(map.getGroups().containsKey(73)); + assertEquals(0, map.getGroups(73).size()); + assertFalse(map.isSetField(73)); + } + + public void testGroupRemovalByFieldBoolean() { + FieldMap map = new Message(); + Group g = new Group(73, 11); + map.addGroup(g); + assertEquals(1, map.getGroups(73).size()); + assertTrue(map.isSetField(73)); + + map.removeGroup(73, true); + + assertFalse(map.getGroups().containsKey(73)); + assertEquals(0, map.getGroups(73).size()); + assertFalse(map.isSetField(73)); + } + private long epochMilliOfLocalDate(LocalDateTime localDateTime) { return localDateTime.toInstant(ZoneOffset.UTC).toEpochMilli(); } @@ -132,4 +178,10 @@ public void testRemoveGroup() { map.removeGroup(73); assertFalse(map.hasGroup(73)); } + + @Test + public void testRemoveMissingField() { + FieldMap map = new Message(); + map.removeField(1); + } } diff --git a/quickfixj-core/src/test/java/quickfix/FieldTest.java b/quickfixj-core/src/test/java/quickfix/FieldTest.java index 06586e41d0..9c6545dee5 100644 --- a/quickfixj-core/src/test/java/quickfix/FieldTest.java +++ b/quickfixj-core/src/test/java/quickfix/FieldTest.java @@ -294,8 +294,8 @@ public void testMultipleStringValue() throws Exception { value.set(new TradeCondition("A B AF AG")); md.addGroup(value); - DataDictionary dd = new DataDictionary("FIX50.xml"); - dd.validate(md); + DataDictionary dd = new DataDictionary("FIX50.xml", true); + dd.validate(md, new ValidationSettings()); } @Test @@ -309,8 +309,8 @@ public void testMultipleCharValue() throws Exception { nos.set(new ExecInst("i V 7")); nos.set(new TransactTime(LocalDateTime.of(2020, 3, 10, 12, 23, 44))); - DataDictionary dd = new DataDictionary("FIX50.xml"); - dd.validate(nos); + DataDictionary dd = new DataDictionary("FIX50.xml", true); + dd.validate(nos, new ValidationSettings()); } private void assertEqualsAndHash(Field field1, Field field2) { diff --git a/quickfixj-core/src/test/java/quickfix/FileLogTest.java b/quickfixj-core/src/test/java/quickfix/FileLogTest.java index 15dd38d6c5..db0a0e1a91 100644 --- a/quickfixj-core/src/test/java/quickfix/FileLogTest.java +++ b/quickfixj-core/src/test/java/quickfix/FileLogTest.java @@ -209,7 +209,7 @@ public void testLogErrorWhenFilesystemRemoved() throws IOException { FileLogFactory factory = new FileLogFactory(settings); try (Session session = new Session(new UnitTestApplication(), new MemoryStoreFactory(), - sessionID, new DefaultDataDictionaryProvider(), null, factory, + sessionID, new DefaultDataDictionaryProvider(), new ValidationSettings(), null, factory, new DefaultMessageFactory(), 0)) { Session.registerSession(session); diff --git a/quickfixj-core/src/test/java/quickfix/FileUtilTest.java b/quickfixj-core/src/test/java/quickfix/FileUtilTest.java index 16cffdc225..b89051aa45 100644 --- a/quickfixj-core/src/test/java/quickfix/FileUtilTest.java +++ b/quickfixj-core/src/test/java/quickfix/FileUtilTest.java @@ -24,7 +24,6 @@ import static org.junit.Assert.assertTrue; import java.io.InputStream; -import java.net.Socket; import org.junit.Test; import org.slf4j.Logger; @@ -55,18 +54,6 @@ public void testClassLoaderResourceLocation() throws Exception { assertNotNull("Resource not found", in); } - @Test - public void testURLLocation() throws Exception { - // Assumption: Internet access - if (isInternetAccessible()) { - InputStream in = FileUtil.open(Message.class, "http://www.quickfixj.org/"); - if (in != null) { - in.close(); - } - assertNotNull("Resource not found", in); - } - } - @Test // QFJ-775 public void testSessionIDFileName() { @@ -81,14 +68,4 @@ public void testSessionIDFileName() { assertEquals("FIX.4.4-SENDER-TARGET", sessionIdFileName); } - private boolean isInternetAccessible() { - try { - Socket socket = new Socket("www.quickfixj.org", 80); - socket.close(); - return true; - } catch (Exception e) { - log.warn("No internet access"); - } - return false; - } } diff --git a/quickfixj-core/src/test/java/quickfix/IncorrectDataFormatTest.java b/quickfixj-core/src/test/java/quickfix/IncorrectDataFormatTest.java new file mode 100644 index 0000000000..646f829345 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/IncorrectDataFormatTest.java @@ -0,0 +1,27 @@ +package quickfix; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class IncorrectDataFormatTest { + + private final int tag = 44; + private final String value = "value"; + + @Test + public void getMessage_provides_field() { + IncorrectDataFormat exception = new IncorrectDataFormat(tag, value); + String message = exception.getMessage(); + + assertTrue(message.contains(Integer.toString(tag))); + } + + @Test + public void getMessage_provides_value() { + IncorrectDataFormat exception = new IncorrectDataFormat(tag, value); + String message = exception.getMessage(); + + assertTrue(message.contains(value)); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/IncorrectTagValueTest.java b/quickfixj-core/src/test/java/quickfix/IncorrectTagValueTest.java new file mode 100644 index 0000000000..481dbe47e4 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/IncorrectTagValueTest.java @@ -0,0 +1,27 @@ +package quickfix; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class IncorrectTagValueTest { + + private final int tag = 44; + private final String value = "value"; + + @Test + public void getMessage_provides_field() { + IncorrectTagValue exception = new IncorrectTagValue(tag, value); + String message = exception.getMessage(); + + assertTrue(message.contains(Integer.toString(tag))); + } + + @Test + public void getMessage_provides_value() { + IncorrectTagValue exception = new IncorrectTagValue(tag, value); + String message = exception.getMessage(); + + assertTrue(message.contains(value)); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java b/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java index 9c1d5147b2..e5884f292c 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java @@ -101,7 +101,7 @@ public void testHandlesRecursivelyFailingException() throws Exception { // need to register the session since we are going to log errors through LogUtil Session.registerSession(new Session(new UnitTestApplication(), new MemoryStoreFactory(), - sessionID, new DefaultDataDictionaryProvider(), null, logFactory, + sessionID, new DefaultDataDictionaryProvider(), new ValidationSettings(), null, logFactory, new DefaultMessageFactory(), 0)); // remove the messages and events tables diff --git a/quickfixj-core/src/test/java/quickfix/LogUtilTest.java b/quickfixj-core/src/test/java/quickfix/LogUtilTest.java index f533c7c042..aee0925266 100644 --- a/quickfixj-core/src/test/java/quickfix/LogUtilTest.java +++ b/quickfixj-core/src/test/java/quickfix/LogUtilTest.java @@ -22,6 +22,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; +import java.util.Calendar; import java.util.Date; import org.junit.After; import static org.junit.Assert.assertTrue; @@ -60,7 +61,7 @@ private void createSessionAndGenerateException(LogFactory mockLogFactory) throws Session session = new Session(null, sessionID1 -> { try { return new MemoryStore() { - public Date getCreationTime() throws IOException { + public Calendar getCreationTimeCalendar() throws IOException { throw new IOException("test"); } }; @@ -68,7 +69,7 @@ public Date getCreationTime() throws IOException { // ignore return null; } - }, sessionID, null, schedule, mockLogFactory, null, 0); + }, sessionID, null, null, schedule, mockLogFactory, null, 0); try { session.close(); } catch (IOException e) { diff --git a/quickfixj-core/src/test/java/quickfix/LoginTestCase.java b/quickfixj-core/src/test/java/quickfix/LoginTestCase.java index 03dad79260..731222d7cc 100644 --- a/quickfixj-core/src/test/java/quickfix/LoginTestCase.java +++ b/quickfixj-core/src/test/java/quickfix/LoginTestCase.java @@ -83,13 +83,13 @@ public TestApplication(SessionID expectedSessionID) { } @Override - public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, + public void fromAdmin(IMessage message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { assertEquals(expectedSessionID, sessionId); } @Override - public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, + public void fromApp(IMessage message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { assertEquals(expectedSessionID, sessionId); } @@ -110,12 +110,12 @@ public void onLogout(SessionID sessionId) { } @Override - public void toAdmin(Message message, SessionID sessionId) { + public void toAdmin(IMessage message, SessionID sessionId) { assertEquals(expectedSessionID, sessionId); } @Override - public void toApp(Message message, SessionID sessionId) throws DoNotSend { + public void toApp(IMessage message, SessionID sessionId) throws DoNotSend { assertEquals(expectedSessionID, sessionId); } } diff --git a/quickfixj-core/src/test/java/quickfix/MessageTest.java b/quickfixj-core/src/test/java/quickfix/MessageTest.java index e6fca46f20..c7209d684f 100644 --- a/quickfixj-core/src/test/java/quickfix/MessageTest.java +++ b/quickfixj-core/src/test/java/quickfix/MessageTest.java @@ -120,21 +120,18 @@ import quickfix.fix50.MarketDataSnapshotFullRefresh; import javax.xml.parsers.DocumentBuilderFactory; +import java.io.File; +import java.io.FileInputStream; import java.math.BigDecimal; +import java.nio.file.Files; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Calendar; import java.util.TimeZone; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; + public class MessageTest { @@ -162,8 +159,8 @@ public void testTrailerFieldOrdering() throws Exception { private NewOrderSingle createNewOrderSingle() { return new NewOrderSingle(new ClOrdID("CLIENT"), new HandlInst( - HandlInst.AUTOMATED_EXECUTION_ORDER_PUBLIC_BROKER_INTERVENTION_OK), new Symbol("ORCL"), - new Side(Side.BUY), new TransactTime(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC)), new OrdType(OrdType.LIMIT)); + HandlInst.AUTOMATED_EXECUTION_ORDER_PUBLIC_BROKER_INTERVENTION_OK), new Symbol("ORCL"), + new Side(Side.BUY), new TransactTime(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC)), new OrdType(OrdType.LIMIT)); } @Test @@ -201,15 +198,14 @@ public MyMessage() { @Test public void testHeaderFieldWithCustomTransportDictionaryConstructorReadsHeaderField() throws Exception { + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(false); - final DataDictionary customSessionDictionary = new DataDictionary("FIXT11_Custom_Test.xml"); - customSessionDictionary.setAllowUnknownMessageFields(false); + final DataDictionary customSessionDictionary = new DataDictionary("FIXT11_Custom_Test.xml", true); - final DataDictionary standardSessionDictionary = new DataDictionary("FIXT11.xml"); - standardSessionDictionary.setAllowUnknownMessageFields(false); + final DataDictionary standardSessionDictionary = new DataDictionary("FIXT11.xml", true); - final DataDictionary applicationDictionary = new DataDictionary("FIX50.xml"); - applicationDictionary.setAllowUnknownMessageFields(false); + final DataDictionary applicationDictionary = new DataDictionary("FIX50.xml", true); final String sep = "\001"; final StringBuilder sb = new StringBuilder(); @@ -243,7 +239,7 @@ public void testHeaderFieldWithCustomTransportDictionaryConstructorReadsHeaderFi sb.append(sep); final String messageData = sb.toString(); - final Message standardMessage = new Message(messageData, standardSessionDictionary, applicationDictionary, true); + final Message standardMessage = new Message(messageData, standardSessionDictionary, applicationDictionary, validationSettings, true); // Test that field is in body not the header assertTrue(standardMessage.toString().contains("12312=foo")); @@ -252,7 +248,7 @@ public void testHeaderFieldWithCustomTransportDictionaryConstructorReadsHeaderFi assertEquals("foo", standardMessage.getString(12312)); // Test that field is correctly classified in header with customSessionDictionary - final Message customMessage = new Message(messageData, customSessionDictionary, applicationDictionary, true); + final Message customMessage = new Message(messageData, customSessionDictionary, applicationDictionary, validationSettings, true); assertTrue(customMessage.toString().contains("12312=foo")); assertTrue(customMessage.getHeader().isSetField(12312)); assertEquals("foo", customMessage.getHeader().getString(12312)); @@ -284,7 +280,7 @@ public void testHeaderGroupParsing() throws Exception { final Message message = new Message("8=FIX.4.2\0019=40\00135=A\001" + "627=2\001628=FOO\001628=BAR\001" + "98=0\001384=2\001372=D\001385=R\001372=8\001385=S\00110=228\001", - DataDictionaryTest.getDictionary()); + DataDictionaryTest.getDictionary(), new ValidationSettings()); final quickfix.fix44.Message.Header.NoHops hops = new quickfix.fix44.Message.Header.NoHops(); message.getHeader().getGroup(1, hops); @@ -305,7 +301,7 @@ public void testEmbeddedMessage() throws Exception { report.set(new EncodedTextLen(text.length())); report.set(new EncodedText(text)); - final Message msg = new Message(report.toString(), DataDictionaryTest.getDictionary()); + final Message msg = new Message(report.toString(), DataDictionaryTest.getDictionary(), new ValidationSettings()); assertEquals("embedded order", text, msg.getString(EncodedText.FIELD)); } @@ -315,7 +311,7 @@ private void doTestMessageWithEncodedField(String charset, String text) throws E NewOrderSingle order = createNewOrderSingle(); order.set(new EncodedTextLen(MessageUtils.length(CharsetSupport.getCharsetInstance(), text))); order.set(new EncodedText(text)); - final Message msg = new Message(order.toString(), DataDictionaryTest.getDictionary()); + final Message msg = new Message(order.toString(), DataDictionaryTest.getDictionary(), new ValidationSettings()); assertEquals(charset + " encoded field", text, msg.getString(EncodedText.FIELD)); } finally { CharsetSupport.setCharset(CharsetSupport.getDefaultCharset()); @@ -339,7 +335,7 @@ public void testParsing() throws Exception { // checksum is not verified in these tests final Message message = new Message("8=FIX.4.2\0019=40\00135=A\001" + "98=0\001384=2\001372=D\001385=R\001372=8\001385=S\00110=96\001", - DataDictionaryTest.getDictionary()); + DataDictionaryTest.getDictionary(), new ValidationSettings()); assertHeaderField(message, "FIX.4.2", BeginString.FIELD); assertHeaderField(message, "40", BodyLength.FIELD); @@ -371,7 +367,7 @@ public void testParsing2() throws Exception { data += "311=IBM\001"; data += "318=CAD\001"; data += "10=037\001"; - final Message message = new Message(data, DataDictionaryTest.getDictionary()); + final Message message = new Message(data, DataDictionaryTest.getDictionary(), new ValidationSettings()); assertHeaderField(message, "FIX.4.2", BeginString.FIELD); assertHeaderField(message, "76", BodyLength.FIELD); @@ -393,7 +389,7 @@ public void testParseEmptyString() throws Exception { // with validation try { - new Message(data, DataDictionaryTest.getDictionary()); + new Message(data, DataDictionaryTest.getDictionary(), new ValidationSettings()); } catch (final InvalidMessage im) { } catch (final Throwable e) { e.printStackTrace(); @@ -402,7 +398,7 @@ public void testParseEmptyString() throws Exception { // without validation try { - new Message(data, DataDictionaryTest.getDictionary(), false); + new Message(data, DataDictionaryTest.getDictionary(), new ValidationSettings(), false); } catch (final InvalidMessage im) { } catch (final Throwable e) { e.printStackTrace(); @@ -410,6 +406,48 @@ public void testParseEmptyString() throws Exception { } } + @Test + public void testParseSeperateDictionaries() throws InvalidMessage, Exception { + String messageString = ("8=FIXT1.1|9=309|35=8|49=ASX|56=CL1_FIX44|34=4|" + + "52=20060324-01:05:58|17=X-B-WOW-1494E9A0:58BD3F9D-1109|150=D|39=0|" + + "11=184271|38=200|198=1494E9A0:58BD3F9D|526=4324|37=B-WOW-1494E9A0:58BD3F9D|" + + "55=WOW|54=1|151=200|14=0|40=2|44=15|59=1|6=0|453=3|448=AAA35791|" + + "447=D|452=3|448=8|447=D|452=4|448=FIX11|" + + "447=D|452=36|60=20060320-03:34:29|10=201|").replaceAll("\\|", "\001"); + DataDictionary fixt11 = DataDictionaryTest.getDictionary("FIXT11.xml"); + DataDictionary fix50sp2 = DataDictionaryTest.getDictionary("FIX50SP2.xml"); + Message message = new Message(messageString, fixt11, fix50sp2, new ValidationSettings()); + assertNull(message.getException()); + } + + @Test + public void testParseSeperateDictionaries_withNoChecksum_hasException() throws InvalidMessage, Exception { + String messageString = ("8=FIXT1.1|9=309|35=8|49=ASX|56=CL1_FIX44|34=4|" + + "52=20060324-01:05:58|17=X-B-WOW-1494E9A0:58BD3F9D-1109|150=D|39=0|" + + "11=184271|38=200|198=1494E9A0:58BD3F9D|526=4324|37=B-WOW-1494E9A0:58BD3F9D|" + + "55=WOW|54=1|151=200|14=0|40=2|44=15|59=1|6=0|453=3|448=AAA35791|" + + "447=D|452=3|448=8|447=D|452=4|448=FIX11|" + + "447=D|452=36|60=20060320-03:34:29|10=|").replaceAll("\\|", "\001"); + DataDictionary fixt11 = DataDictionaryTest.getDictionary("FIXT11.xml"); + DataDictionary fix50sp2 = DataDictionaryTest.getDictionary("FIX50SP2.xml"); + Message message = new Message(messageString, fixt11, fix50sp2, new ValidationSettings()); + assertEquals(FieldException.class, message.getException().getClass()); + } + + @Test + public void testParseSeperateDictionaries_noValidation() throws InvalidMessage, Exception { + String messageString = ("8=FIXT1.1|9=309|35=8|49=ASX|56=CL1_FIX44|34=4|" + + "52=20060324-01:05:58|17=X-B-WOW-1494E9A0:58BD3F9D-1109|150=D|39=0|" + + "11=184271|38=200|198=1494E9A0:58BD3F9D|526=4324|37=B-WOW-1494E9A0:58BD3F9D|" + + "55=WOW|54=1|151=200|14=0|40=2|44=15|59=1|6=0|453=3|448=AAA35791|" + + "447=D|452=3|448=8|447=D|452=4|448=FIX11|" + + "447=D|452=36|60=20060320-03:34:29|10=|").replaceAll("\\|", "\001"); + DataDictionary fixt11 = DataDictionaryTest.getDictionary("FIXT11.xml"); + DataDictionary fix50sp2 = DataDictionaryTest.getDictionary("FIX50SP2.xml"); + Message message = new Message(messageString, fixt11, fix50sp2, new ValidationSettings(), false); + assertNull(message.getException()); + } + @Test public void testValidation() throws Exception { final String data = "8=FIX.4.4\0019=309\00135=8\00149=ASX\00156=CL1_FIX44\00134=4\001" + @@ -421,8 +459,8 @@ public void testValidation() throws Exception { final ExecutionReport executionReport = new ExecutionReport(); final DataDictionary dictionary = DataDictionaryTest.getDictionary(); assertNotNull(dictionary); - executionReport.fromString(data, dictionary, true); - dictionary.validate(executionReport); + executionReport.fromString(data, dictionary, new ValidationSettings(), true); + dictionary.validate(executionReport, new ValidationSettings()); } @Test @@ -445,12 +483,12 @@ public void testParseTwice() throws Exception { final ExecutionReport executionReport = new ExecutionReport(); assertNotNull(dictionary); - executionReport.fromString(data1, dictionary, true); - dictionary.validate(executionReport); + executionReport.fromString(data1, dictionary, new ValidationSettings(), true); + dictionary.validate(executionReport, new ValidationSettings()); executionReport.clear(); - executionReport.fromString(data2, dictionary, true); - dictionary.validate(executionReport); + executionReport.fromString(data2, dictionary, new ValidationSettings(), true); + dictionary.validate(executionReport, new ValidationSettings()); } @Test @@ -465,12 +503,12 @@ public void testValidationWithHops() throws Exception { final DataDictionary dictionary = DataDictionaryTest.getDictionary(); assertNotNull(dictionary); - executionReport.fromString(data, dictionary, true); + executionReport.fromString(data, dictionary, new ValidationSettings(), true); final Header.NoHops hops = new Header.NoHops(); hops.set(new HopCompID("FOO")); executionReport.header.addGroup(hops); - dictionary.validate(executionReport); + dictionary.validate(executionReport, new ValidationSettings()); } @Test @@ -483,8 +521,8 @@ public void testAppMessageValidation() throws Exception { final DataDictionary appDictionary = DataDictionaryTest.getDictionary("FIX50.xml"); assertNotNull(sessDictionary); assertNotNull(appDictionary); - mdsfr.fromString(data, sessDictionary, appDictionary, true); - DataDictionary.validate(mdsfr, sessDictionary, appDictionary); + mdsfr.fromString(data, sessDictionary, appDictionary, new ValidationSettings(), true); + DataDictionary.validate(mdsfr, sessDictionary, appDictionary, new ValidationSettings()); } @Test @@ -496,8 +534,8 @@ public void testAdminMessageValidation() throws Exception { final DataDictionary appDictionary = DataDictionaryTest.getDictionary("FIX50.xml"); assertNotNull(sessionDictionary); assertNotNull(appDictionary); - logon.fromString(data, sessionDictionary, appDictionary, true); - DataDictionary.validate(logon, sessionDictionary, sessionDictionary); + logon.fromString(data, sessionDictionary, appDictionary, new ValidationSettings(), true); + DataDictionary.validate(logon, sessionDictionary, sessionDictionary, new ValidationSettings()); } @Test @@ -547,7 +585,7 @@ public void testComponentGroupInsertion() throws Exception { public void testHeaderDataField() throws Exception { final Message m = new Message("8=FIX.4.2\0019=53\00135=A\00190=4\00191=ABCD\001" + "98=0\001384=2\001372=D\001385=R\001372=8\001385=S\00110=241\001", - DataDictionaryTest.getDictionary()); + DataDictionaryTest.getDictionary(), new ValidationSettings()); assertEquals("ABCD", m.getHeader().getString(SecureData.FIELD)); } @@ -562,7 +600,7 @@ public void testInvalidFirstFieldInGroup() throws Exception { news.addGroup(relatedSym); try { - new Message(news.toString(), DataDictionaryTest.getDictionary()); + new Message(news.toString(), DataDictionaryTest.getDictionary(), new ValidationSettings()); } catch (final InvalidMessage e) { // expected } catch (final NullPointerException e) { @@ -576,7 +614,7 @@ public void testRequiredGroupValidation() throws Exception { news.set(new Headline("Test")); final DataDictionary dictionary = DataDictionaryTest.getDictionary(); try { - dictionary.validate(news); + dictionary.validate(news, new ValidationSettings()); fail("no field exception for missing lines group"); } catch (final FieldException e) { // expected @@ -608,9 +646,9 @@ public void testDataFieldParsing() throws Exception { final DataDictionary dictionary = DataDictionaryTest.getDictionary(); final Message m = new Message(("8=FIX.4.4\0019=1144\00135=A\001" + "98=0\001384=2\001372=D\001385=R\001372=8\001385=S\00195=1092\001" + "96=" - + data + "\00110=5\001"), dictionary); + + data + "\00110=5\001"), dictionary, new ValidationSettings()); assertEquals(1144, m.bodyLength()); - final Message m2 = new Message(m.toString(), dictionary); + final Message m2 = new Message(m.toString(), dictionary, new ValidationSettings()); assertEquals(1144, m2.bodyLength()); } catch (final InvalidMessage e) { fail(e.getMessage()); @@ -647,7 +685,7 @@ public void testDataFieldWithManualFieldInsertion() throws Exception { m.setInt(RawDataLength.FIELD, data.length()); m.setString(RawData.FIELD, data); assertEquals(1108 + msgType.getValue().length(), m.bodyLength()); - final Message m2 = new Message(m.toString(), dictionary); + final Message m2 = new Message(m.toString(), dictionary, new ValidationSettings()); assertEquals(m.bodyLength(), m2.bodyLength()); } catch (final InvalidMessage e) { fail(e.getMessage()); @@ -682,7 +720,7 @@ public void testCalculateStringWithNestedGroups() throws Exception { noc.setString(TransactTime.FIELD, "20060319-09:08:19"); noc.setString(CrossID.FIELD, "184214"); noc.setInt(CrossType.FIELD, - CrossType.CROSS_IOC_CROSS_TRADE_WHICH_IS_EXECUTED_PARTIALLY_AND_THE_REST_IS_CANCELLED_ONE_SIDE_IS_FULLY_EXECUTED_THE_OTHER_SIDE_IS_PARTIALLY_EXECUTED_WITH_THE_REMAINDER_BEING_CANCELLED_THIS_IS_EQUIVALENT_TO_AN_IOC_ON_THE_OTHER_SIDE_NOTE_CROSSPRIORITIZATION_FIELD_MAY_BE_USED_TO_INDICATE_WHICH_SIDE_SHOULD_FULLY_EXECUTE_IN_THIS_SCENARIO_); + CrossType.CROSS_IOC_CROSS_TRADE_WHICH_IS_EXECUTED_PARTIALLY_AND_THE_REST_IS_CANCELLED_ONE_SIDE_IS_FULLY_EXECUTED_THE_OTHER_SIDE_IS_PARTIALLY_EXECUTED_WITH_THE_REMAINDER_BEING_CANCELLED_THIS_IS_EQUIVALENT_TO_AN_IOC_ON_THE_OTHER_SIDE_NOTE_CROSSPRIORITIZATION_FIELD_MAY_BE_USED_TO_INDICATE_WHICH_SIDE_SHOULD_FULLY_EXECUTE_IN_THIS_SCENARIO_); noc.setInt(CrossPrioritization.FIELD, CrossPrioritization.NONE); final NewOrderCross.NoSides side = new NewOrderCross.NoSides(); @@ -722,22 +760,22 @@ public void testCalculateStringWithNestedGroups() throws Exception { noc.addGroup(side); - final String expectedMessage = "8=FIX.4.4\0019=247\00135=s\00134=5\00149=sender\00152=20060319-09:08:20.881\001" - + "56=target\00122=8\00140=2\00144=9\00148=ABC\00155=ABC\00160=20060319-09:08:19\001548=184214\001549=2\001" - + "550=0\001552=2\00154=1\001453=2\001448=8\001447=D\001452=4\001448=AAA35777\001447=D\001452=3\00138=9\00154=2\001" - + "453=2\001448=8\001447=D\001452=4\001448=aaa\001447=D\001452=3\00138=9\00110=056\001"; + final String expectedMessage = "8=FIX.4.4\0019=247\00135=s\00134=5\00149=sender\00156=target\001" + + "52=20060319-09:08:20.881\00122=8\00140=2\00144=9\00148=ABC\00155=ABC\00160=20060319-09:08:19\001548=184214\001549=2\001" + + "550=0\001552=2\00154=1\001453=2\001448=8\001447=D\001452=4\001448=AAA35777\001447=D\001452=3\00138=9\00154=2\001" + + "453=2\001448=8\001447=D\001452=4\001448=aaa\001447=D\001452=3\00138=9\00110=056\001"; assertEquals("wrong message", expectedMessage, noc.toString()); } @Test public void testFieldOrdering() throws Exception { final String expectedMessageString = "8=FIX.4.4\0019=171\00135=D\00149=SenderCompId\00156=TargetCompId\001" + - "11=183339\00122=8\00138=1\00140=2\00144=12\00148=BHP\00154=2\00155=BHP\00159=1\00160=20060223-22:38:33\001" + - "526=3620\001453=2\001448=8\001447=D\001452=4\001448=AAA35354\001447=D\001452=3\00110=168\001"; - final DataDictionary dataDictionary = new DataDictionary("FIX44.xml"); + "11=183339\00122=8\00138=1\00140=2\00144=12\00148=BHP\00154=2\00155=BHP\00159=1\00160=20060223-22:38:33\001" + + "526=3620\001453=2\001448=8\001447=D\001452=4\001448=AAA35354\001447=D\001452=3\00110=168\001"; + final DataDictionary dataDictionary = new DataDictionary("FIX44.xml", true); final Message message = new DefaultMessageFactory() .create(dataDictionary.getVersion(), "D"); - message.fromString(expectedMessageString, dataDictionary, false); + message.fromString(expectedMessageString, dataDictionary, new ValidationSettings(), false); final String actualMessageString = message.toString(); assertTrue( "wrong field ordering", @@ -757,7 +795,7 @@ public void testHeaderFieldsMissing() throws Exception { public void testHeaderFieldInBody() throws Exception { final Message message = new Message("8=FIX.4.2\0019=40\00135=A\001" + "98=0\001212=4\001384=2\001372=D\001385=R\001372=8\001385=S\00110=103\001", - DataDictionaryTest.getDictionary()); + DataDictionaryTest.getDictionary(), new ValidationSettings()); assertFalse(message.hasValidStructure()); @@ -772,7 +810,7 @@ public void testHeaderFieldInBody() throws Exception { public void testTrailerFieldInBody() throws Exception { final Message message = new Message("8=FIX.4.2\0019=40\00135=A\001" + "98=0\00193=5\001384=2\001372=D\001385=R\001372=8\001385=S\00110=63\001", - DataDictionaryTest.getDictionary()); + DataDictionaryTest.getDictionary(), new ValidationSettings()); assertFalse(message.hasValidStructure()); @@ -818,14 +856,13 @@ public void testMessageGroupCountValidation() throws Exception { "79=AllocACC2\00180=2020.2\001453=2\001448=8\001447=D\001452=4\001448=AAA35354\001447=D\001452=3\00110=079\001"; final Message message = new Message(); final DataDictionary dd = DataDictionaryTest.getDictionary(); - message.fromString(data, dd, true); + message.fromString(data, dd, new ValidationSettings(), true); try { - dd.validate(message); + dd.validate(message, new ValidationSettings()); fail("No exception thrown"); } catch (final FieldException e) { final String emsg = e.getMessage(); assertNotNull("No exception message", emsg); - assertTrue(emsg.startsWith("Incorrect NumInGroup")); } } @@ -843,7 +880,7 @@ public void testMessageWithMissingChecksumField() throws Exception { Message msg = new Message(); try { - msg.fromString(badMessage, DataDictionaryTest.getDictionary(), true); + msg.fromString(badMessage, DataDictionaryTest.getDictionary(), new ValidationSettings(), true); fail(); } catch (final InvalidMessage e) { final String emsg = e.getMessage(); @@ -1324,7 +1361,7 @@ public void testFalseMessageStructureException() { final DataDictionary dd = DataDictionaryTest.getDictionary(); // duplicated tag 98 // QFJ-65 - new Message("8=FIX.4.4\0019=22\00135=A\00198=0\00198=0\001108=30\00110=223\001", dd, + new Message("8=FIX.4.4\0019=22\00135=A\00198=0\00198=0\001108=30\00110=223\001", dd, new ValidationSettings(), true); // For now, this will not cause an exception if the length and checksum are correct } catch (final Exception e) { @@ -1353,23 +1390,23 @@ public void testComponentInGroup() { // 602=BRN FMG0010! 63=8 608-FXXXXX 624=1 637=80.09 687=1.0 654=41296073 9019=1 9023=1 9020=20100201 9021=20100228 539=4 524=805\001 // 525=D\001538=4\001524=11122556 525=D\001538=51 524=Newedge 525=D 538=60 524=U 525=D 538=54 10=112 new Message( - "8=FIX.4.4\0019=941\00135=AE\00149=ICE\00134=63\00152=20091117-18:59:04.780\00156=XXXX\001" + - "57=X\001571=219449\001487=0\001856=0\001828=0\001150=F\00117=44750544433\00139=2\001" + - "570=N\00155=480120\00148=WBS FMG0010-BRN FMG0010\00122=8\001461=FXXXXX\001916=20100201\001" + - "917=20100228\00132=1.0\00131=0.69\0019018=1\0019022=1\00175=20091117\00160=20091117-18:59:04.775\001" + - "552=1\00154=2\00137=41296064\00111=557859232\001453=7\001448=trader\001447=D\001452=11\001" + - "448=Trading Corp\001447=D\001452=13\001448=2757\001447=D\001452=56\001448=805\001447=D\001" + - "452=4\001448=11122556\001447=D\001452=51\001448=FCM\001447=D\001452=60\001448=U\001447=D\001" + - "452=5 4\00158=41293051\001555=2\001600=460130\001602=WBS FMG0010!\001603=8\001608=FXXXXX\001" + - "624=2\001637=80.78\001687=1.0\001654=41296074\0019019=1\0019023=1\0019020=20100201\001" + - "9021=20100228\001539=4\001524=805\001525=D\001538=4\001524=11122556\001525=D\001538=51\001" + - "524=FCM\001525=D\001538=60 524=U\001525=D\001538=54\001600=217927\001602=BRN FMG0010!\001" + - "63=8 608-FXXXXX\001624=1\001637=80.09\001687=1.0\001654=41296073\0019019=1\0019023=1\001" + - "9020=20100201\001021=20100228\001539=4\001524=805\001525=D\001538=4\001524=11122556\001" + - "525=D\001538=51\001524=FCM\001525=D\001538=60 524=U\001525=D\001538=54\001600=217927\001" + - "602=BRN FMG0010!\00163=8 608-FXXXXX\001624=1\001637=80.09\001687=1.0\001654=41296073\001" + - "9019=1\0019023=1\0019020=20100201\001021=20100228\001", - dd, true); + "8=FIX.4.4\0019=941\00135=AE\00149=ICE\00134=63\00152=20091117-18:59:04.780\00156=XXXX\001" + + "57=X\001571=219449\001487=0\001856=0\001828=0\001150=F\00117=44750544433\00139=2\001" + + "570=N\00155=480120\00148=WBS FMG0010-BRN FMG0010\00122=8\001461=FXXXXX\001916=20100201\001" + + "917=20100228\00132=1.0\00131=0.69\0019018=1\0019022=1\00175=20091117\00160=20091117-18:59:04.775\001" + + "552=1\00154=2\00137=41296064\00111=557859232\001453=7\001448=trader\001447=D\001452=11\001" + + "448=Trading Corp\001447=D\001452=13\001448=2757\001447=D\001452=56\001448=805\001447=D\001" + + "452=4\001448=11122556\001447=D\001452=51\001448=FCM\001447=D\001452=60\001448=U\001447=D\001" + + "452=5 4\00158=41293051\001555=2\001600=460130\001602=WBS FMG0010!\001603=8\001608=FXXXXX\001" + + "624=2\001637=80.78\001687=1.0\001654=41296074\0019019=1\0019023=1\0019020=20100201\001" + + "9021=20100228\001539=4\001524=805\001525=D\001538=4\001524=11122556\001525=D\001538=51\001" + + "524=FCM\001525=D\001538=60 524=U\001525=D\001538=54\001600=217927\001602=BRN FMG0010!\001" + + "63=8 608-FXXXXX\001624=1\001637=80.09\001687=1.0\001654=41296073\0019019=1\0019023=1\001" + + "9020=20100201\001021=20100228\001539=4\001524=805\001525=D\001538=4\001524=11122556\001" + + "525=D\001538=51\001524=FCM\001525=D\001538=60 524=U\001525=D\001538=54\001600=217927\001" + + "602=BRN FMG0010!\00163=8 608-FXXXXX\001624=1\001637=80.09\001687=1.0\001654=41296073\001" + + "9019=1\0019023=1\0019020=20100201\001021=20100228\001", + dd, new ValidationSettings(), true); // For now, this will not cause an exception if the length and checksum are correct } catch (final Exception e) { final String text = e.getMessage(); @@ -1383,7 +1420,7 @@ public void testFalseMessageStructureException2() { final DataDictionary dd = DataDictionaryTest.getDictionary(); // duplicated raw data length // QFJ-121 - new Message("8=FIX.4.4\0019=22\00135=A\00196=X\001108=30\00110=223\001", dd, true); + new Message("8=FIX.4.4\0019=22\00135=A\00196=X\001108=30\00110=223\001", dd, new ValidationSettings(), true); } catch (final Exception e) { final String text = e.getMessage(); assertTrue("Wrong exception message: " + text, @@ -1396,13 +1433,13 @@ public void testFieldWithEqualsCharacter() { try { final DataDictionary dd = DataDictionaryTest.getDictionary(); final Message m = new Message( - "8=FIXT.1.1\0019=369\00135=W\00149=I\00156=F\00134=4\00152=20111021-15:09:16.535\001" + - "262=1319209757316210\00121=2\00155=EUR/USD\001461=RCSXX=0\001268=8\001" + - "269=0\001270=1.38898\001271=2000000\001269=0\001270=1.38897\001271=8000000\001" + - "269=0\001270=1.38854\001271=2000000\001269=1\001270=1.38855\001271=6000000\001" + - "269=1\001270=1.38856\001271=7000000\001269=1\001270=1.38857\001271=3000000\001" + - "269=1\001270=1.38858\001271=9000000\001269=1\001270=1.38859\001271=100000000\00110=51\001", - dd, true); + "8=FIXT.1.1\0019=369\00135=W\00149=I\00156=F\00134=4\00152=20111021-15:09:16.535\001" + + "262=1319209757316210\00121=2\00155=EUR/USD\001461=RCSXX=0\001268=8\001" + + "269=0\001270=1.38898\001271=2000000\001269=0\001270=1.38897\001271=8000000\001" + + "269=0\001270=1.38854\001271=2000000\001269=1\001270=1.38855\001271=6000000\001" + + "269=1\001270=1.38856\001271=7000000\001269=1\001270=1.38857\001271=3000000\001" + + "269=1\001270=1.38858\001271=9000000\001269=1\001270=1.38859\001271=100000000\00110=51\001", + dd, new ValidationSettings(), true); assertEquals(m.getString(461), "RCSXX=0"); final MarketDataSnapshotFullRefresh.NoMDEntries group = new MarketDataSnapshotFullRefresh.NoMDEntries(); m.getGroup(1, group); @@ -1421,13 +1458,13 @@ public void testMiscFeeType() { try { final DataDictionary dd = DataDictionaryTest.getDictionary(); final Message m = new Message( - "8=FIXT.1.1\0019=369\00135=W\00149=I\00156=F\00134=4\00152=20111021-15:09:16.535\001" + - "262=1319209757316210\00121=2\00155=EUR/USD\001461=RCSXX=0\001268=8\001" + - "269=0\001270=1.38898\001271=2000000\001269=0\001270=1.38897\001271=8000000\001" + - "269=0\001270=1.38854\001271=2000000\001269=1\001270=1.38855\001271=6000000\001" + - "269=1\001270=1.38856\001271=7000000\001269=1\001270=1.38857\001271=3000000\001" + - "269=1\001270=1.38858\001271=9000000\001269=1\001270=1.38859\001271=100000000\00110=51\001", - dd, true); + "8=FIXT.1.1\0019=369\00135=W\00149=I\00156=F\00134=4\00152=20111021-15:09:16.535\001" + + "262=1319209757316210\00121=2\00155=EUR/USD\001461=RCSXX=0\001268=8\001" + + "269=0\001270=1.38898\001271=2000000\001269=0\001270=1.38897\001271=8000000\001" + + "269=0\001270=1.38854\001271=2000000\001269=1\001270=1.38855\001271=6000000\001" + + "269=1\001270=1.38856\001271=7000000\001269=1\001270=1.38857\001271=3000000\001" + + "269=1\001270=1.38858\001271=9000000\001269=1\001270=1.38859\001271=100000000\00110=51\001", + dd, new ValidationSettings(), true); assertEquals(m.getString(461), "RCSXX=0"); final MarketDataSnapshotFullRefresh.NoMDEntries group = new MarketDataSnapshotFullRefresh.NoMDEntries(); m.getGroup(1, group); @@ -1508,7 +1545,7 @@ public void testRepeatingGroupCount() throws Exception { m1.addGroup(leg2); String s1 = m1.toString(); - Message parsed1 = new Message(s1, DataDictionaryTest.getDictionary()); + Message parsed1 = new Message(s1, DataDictionaryTest.getDictionary(), new ValidationSettings()); assertEquals(s1, parsed1.toString()); assertEquals(2, parsed1.getGroupCount(555)); @@ -1531,7 +1568,7 @@ public void testRepeatingGroupCount() throws Exception { String s2 = m2.toString(); // do not use validation to parse full message // regardless of errors in message structure - Message parsed2 = new Message(s2, DataDictionaryTest.getDictionary(), false); + Message parsed2 = new Message(s2, DataDictionaryTest.getDictionary(), new ValidationSettings(), false); assertEquals(s2, parsed2.toString()); assertEquals(2, parsed2.getGroupCount(555)); @@ -1582,22 +1619,23 @@ public void testUnknownFieldsInRepeatingGroupsAndValidation() throws Exception { m1.addGroup(sides); String s1 = m1.toString(); + ValidationSettings validationSettings = new ValidationSettings(); DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); // parsing without validation should succeed - Message parsed1 = new Message(s1, dictionary, false); + Message parsed1 = new Message(s1, dictionary, validationSettings,false); // validation should fail int failingTag = 0; try { - dictionary.validate(parsed1); + dictionary.validate(parsed1, validationSettings); } catch (FieldException e) { failingTag = e.getField(); } assertEquals(10000, failingTag); // but without checking user-defined fields, validation should succeed - dictionary.setCheckUserDefinedFields(false); - dictionary.validate(parsed1); + validationSettings.setCheckUserDefinedFields(false); + dictionary.validate(parsed1, validationSettings); assertEquals(s1, parsed1.toString()); assertEquals(2, parsed1.getGroupCount(555)); @@ -1619,24 +1657,56 @@ public void testUnknownFieldsInRepeatingGroupsAndValidation() throws Exception { String s2 = m2.toString(); DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); // parsing without validation should succeed - Message parsed2 = new Message(s2, dictionary, false); + Message parsed2 = new Message(s2, dictionary, new ValidationSettings(), false); // validation should fail int failingTag = 0; try { - dictionary.validate(parsed2); + dictionary.validate(parsed2, new ValidationSettings()); } catch (FieldException e) { failingTag = e.getField(); } assertEquals(Text.FIELD, failingTag); // but without checking for unknown message fields, validation should succeed - dictionary.setAllowUnknownMessageFields(true); - dictionary.validate(parsed2); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(true); + dictionary.validate(parsed2, validationSettings); assertEquals(s2, parsed2.toString()); assertEquals(2, parsed2.getGroupCount(555)); } + + } + + @Test + public void testUnknownFirstField() throws Exception { + Message tcr = new TradeCaptureReport(new TradeReportID("ABC1234"), new PreviouslyReported( + false), new LastQty(1000), new LastPx(5.6789), new TradeDate("20140101"), + new TransactTime(LocalDateTime.now(ZoneOffset.UTC))); + Message m3 = new Message(); + m3.getHeader().setFields(tcr.getHeader()); + m3.setFields(tcr); + + TradeCaptureReport.NoLegs legBad = new TradeCaptureReport.NoLegs(); + legBad.setString(3002, "ABC"); // add unknown tag to leg + + m3.addGroup(legBad); + + String s3 = m3.toString(); + DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); + // parsing without validation should succeed + ValidationSettings validationSettings = new ValidationSettings(); + Message parsed2 = new Message(s3, dictionary, validationSettings, false); + + // validation should fail + int failingTag = 0; + try { + dictionary.validate(parsed2, validationSettings); + } catch (FieldException e) { + failingTag = e.getField(); + } + assertEquals(3002, failingTag); } @Test @@ -1671,9 +1741,10 @@ public void testInvalidFieldInGroup() throws Exception { DataDictionary dd = new DataDictionary(DataDictionaryTest.getDictionary()); + ValidationSettings validationSettings = new ValidationSettings(); int tagNo = 0; try { - dd.validate(responseMessage, true); + dd.validate(responseMessage, true, validationSettings); } catch (FieldException e) { tagNo = e.getField(); } @@ -1681,9 +1752,9 @@ public void testInvalidFieldInGroup() throws Exception { // (which is the first field after the invalid 297 field) assertEquals(QuoteAckStatus.FIELD, tagNo); - Message msg2 = new Message(responseMessage.toString(), dd); + Message msg2 = new Message(responseMessage.toString(), dd, validationSettings); try { - dd.validate(msg2, true); + dd.validate(msg2, true, validationSettings); } catch (FieldException e) { tagNo = e.getField(); } @@ -1692,7 +1763,7 @@ public void testInvalidFieldInGroup() throws Exception { assertEquals(QuoteAckStatus.FIELD, tagNo); // parse message again without validation - msg2 = new Message(responseMessage.toString(), dd, false); + msg2 = new Message(responseMessage.toString(), dd, validationSettings, false); assertEquals(responseMessage.toString(), msg2.toString()); Group noRelatedSymGroup = new quickfix.fix44.DerivativeSecurityList.NoRelatedSym(); Group group = responseMessage.getGroup(1, noRelatedSymGroup); @@ -1715,11 +1786,12 @@ public void testNestedRepeatingGroup() quickfix.fix44.NewOrderSingle nos = new quickfix.fix44.NewOrderSingle(); // using custom dictionary with user-defined tag 22000 - final DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setCheckUserDefinedFields(false); - nos.fromString(newOrdersSingleString.replaceAll("\\|", "\001"), dataDictionary, true); + final DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setCheckUserDefinedFields(false); + nos.fromString(newOrdersSingleString.replaceAll("\\|", "\001"), dataDictionary, validationSettings, true); assertNull(nos.getException()); - dataDictionary.validate(nos); + dataDictionary.validate(nos, validationSettings); // defined tag should be set on the message assertTrue(nos.isSetField(22000)); @@ -1744,10 +1816,11 @@ public void testUnknownTagBeforeFirstFieldInRepeatingGroup() quickfix.fix44.NewOrderSingle nos = new quickfix.fix44.NewOrderSingle(); final DataDictionary dataDictionary = new DataDictionary(DataDictionaryTest.getDictionary()); - dataDictionary.setCheckUserDefinedFields(false); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setCheckUserDefinedFields(false); // When - nos.fromString(newOrdersSingleString.replaceAll("\\|", "\001"), dataDictionary, true); + nos.fromString(newOrdersSingleString.replaceAll("\\|", "\001"), dataDictionary, validationSettings, true); // Then FieldException e = nos.getException(); @@ -1769,11 +1842,12 @@ public void testNestedRepeatingSubGroup() quickfix.fix44.NewOrderSingle nos = new quickfix.fix44.NewOrderSingle(); // using custom dictionary with user-defined tag 22000 - final DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); - dataDictionary.setCheckUserDefinedFields(false); - nos.fromString(newOrdersSingleString.replaceAll("\\|", "\001"), dataDictionary, true); + final DataDictionary dataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setCheckUserDefinedFields(false); + nos.fromString(newOrdersSingleString.replaceAll("\\|", "\001"), dataDictionary, validationSettings, true); assertNull(nos.getException()); - dataDictionary.validate(nos); + dataDictionary.validate(nos, validationSettings); // defined tag should be set on the message assertTrue(nos.isSetField(22000)); @@ -1788,7 +1862,7 @@ public void testNestedRepeatingSubGroup() @Test // QFJ-792 public void testRepeatingGroupCountForIncorrectFieldOrder() throws Exception { - // correct order would be 600, 687, 654, 566 + // correct order would be 600, 687, 654, 566 testRepeatingGroupCountForFieldOrder(new int[]{600, 687, 566, 654}); } @@ -1820,9 +1894,10 @@ private void testRepeatingGroupCountForFieldOrder(int fieldOrder[]) throws Excep */ String s = tcr.toString(); DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); - dictionary.setCheckUnorderedGroupFields(false); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setCheckUnorderedGroupFields(false); // without checking order of repeating group it should work - Message parsed = new Message(s, dictionary); + Message parsed = new Message(s, dictionary, validationSettings); FieldException exception = parsed.getException(); assertNull(exception); @@ -1830,8 +1905,11 @@ private void testRepeatingGroupCountForFieldOrder(int fieldOrder[]) throws Excep dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); // when checking order of repeating group, an error should be reported - parsed = new Message(s, dictionary); + ValidationSettings validationSettings1 = new ValidationSettings(); + validationSettings1.setCheckUnorderedGroupFields(true); + parsed = new Message(s, dictionary, validationSettings1); exception = parsed.getException(); + assertNotNull(exception); assertEquals(654, exception.getField()); // but we still should have the repeating group set and not ignore it assertEquals(1, parsed.getGroupCount(555)); @@ -1841,11 +1919,12 @@ private void testRepeatingGroupCountForFieldOrder(int fieldOrder[]) throws Excep @Test public void testRepeatingGroupCountWithNonIntegerValues() throws Exception { DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); + ValidationSettings validationSettings = new ValidationSettings(); Message ioi = new quickfix.fix50.IOI(); ioi.setString(quickfix.field.NoPartyIDs.FIELD, "abc"); final String invalidCountMessage = ioi.toString(); try { - Message message = new Message(invalidCountMessage, dictionary); + Message message = new Message(invalidCountMessage, dictionary, validationSettings); } catch (final InvalidMessage im) { assertNotNull("InvalidMessage correctly thrown", im); } catch (final Throwable e) { @@ -1865,8 +1944,11 @@ public void testRepeatingGroupCountWithUnknownFields() throws Exception { + "9083=0|9084=0|9061=579|9062=text|9063=text|9032=10.0|9002=F|9004=780415|9005=780503|10=223|"; DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); + ValidationSettings validationSettings = new ValidationSettings(); + validationSettings.setAllowUnknownMessageFields(false); Message message = new Message(); - message.fromString(test.replaceAll("\\|", "\001"), dictionary, true); + message.fromString(test.replaceAll("\\|", "\001"), dictionary, validationSettings, true); + assertTrue(message.getGroups(711).size() >= 1); Group group = message.getGroup(1, 711); String underlyingSymbol = group.getString(311); assertEquals("780508", underlyingSymbol); @@ -1884,7 +1966,7 @@ public void testRawString() throws Exception { DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); Message message = new Message(); - message.fromString(test.replaceAll("\\|", "\001"), dictionary, true); + message.fromString(test.replaceAll("\\|", "\001"), dictionary, new ValidationSettings(), true); assertEquals(test, message.toRawString().replaceAll("\001", "\\|")); } @@ -1909,6 +1991,7 @@ public void testIfMessageHeaderIsOverwritten() { public void testIfMessageHeaderIsCreatedWithEveryConstructor() throws Exception { final String rawMessage = "8=FIX.4.2\0019=12\00135=A\001108=30\00110=026\001"; final DataDictionary dataDictionary = new DataDictionary(DataDictionaryTest.getDictionary()); + final ValidationSettings validationSettings = new ValidationSettings(); final Message emptyConstructor = new Message(); assertNotNull(emptyConstructor.getHeader()); @@ -1922,13 +2005,13 @@ public void testIfMessageHeaderIsCreatedWithEveryConstructor() throws Exception final Message fourthConstructor = new Message(rawMessage, false); assertNotNull(fourthConstructor.getHeader()); - final Message fifthConstructor = new Message(rawMessage, dataDictionary); + final Message fifthConstructor = new Message(rawMessage, dataDictionary, validationSettings); assertNotNull(fifthConstructor.getHeader()); - final Message sixthConstructor = new Message(rawMessage, dataDictionary, false); + final Message sixthConstructor = new Message(rawMessage, dataDictionary, validationSettings,false); assertNotNull(sixthConstructor.getHeader()); - final Message seventhConstructor = new Message(rawMessage, dataDictionary, dataDictionary, false); + final Message seventhConstructor = new Message(rawMessage, dataDictionary, dataDictionary, validationSettings, false); assertNotNull(seventhConstructor.getHeader()); } @@ -1989,15 +2072,15 @@ public void testValidateFieldsOutOfOrderFIXT11() throws Exception { "54=2\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u000111=7533509260093757876\u0001" + "10=129\u0001"; final TradeCaptureReport tcrOrdered = new TradeCaptureReport(); - tcrOrdered.fromString(orderedData, sessDictionary, appDictionary, true); - DataDictionary.validate(tcrOrdered, sessDictionary, appDictionary); + ValidationSettings validationSettings = new ValidationSettings(); + tcrOrdered.fromString(orderedData, sessDictionary, appDictionary, validationSettings, true); + DataDictionary.validate(tcrOrdered, sessDictionary, appDictionary, validationSettings); // As this is our reference message created with all validations switched on, make sure some message components // are as expected assertEquals(tcrOrdered.getHeader().getGroupCount(NoHops.FIELD), 2); assertEquals(tcrOrdered.getGroupCount(NoSides.FIELD), 2); - sessDictionary.setCheckFieldsOutOfOrder(false); - appDictionary.setCheckFieldsOutOfOrder(false); + validationSettings.setCheckFieldsOutOfOrder(false); String unorderedData = "8=FIXT.1.1\u00019=561\u000135=AE\u0001" + "15=AUD\u000122=4\u000131=27\u000132=5000.000000000000\u000148=AU000000ANZ3\u000155=ANZ\u000160=20220210-02:43:27.796\u000164=20220214\u000175=20220210\u0001106=4075\u0001167=CS\u0001381=135000\u0001461=Exxxxx\u0001487=0\u0001762=1\u0001880=7533509260093686098:0#NORMAL#1644451200000000000\u00011003=1120000338\u00011015=0\u00011301=XASX\u0001" + @@ -2009,8 +2092,8 @@ public void testValidateFieldsOutOfOrderFIXT11() throws Exception { "34=545\u000149=SENDER\u000152=20220210-02:44:00.820\u000156=TARGET\u0001115=ON_BHEHALF\u00011128=9\u0001" + "10=129\u0001"; TradeCaptureReport tcrUnOrdered = new TradeCaptureReport(); - tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary); + tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, validationSettings, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary, validationSettings); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -2024,8 +2107,8 @@ public void testValidateFieldsOutOfOrderFIXT11() throws Exception { "627=2\u0001628=HOPID1\u0001629=20220414-15:22:54\u0001628=HOPID2\u0001629=20220414-15:22:54\u0001" + "10=129\u0001"; tcrUnOrdered = new TradeCaptureReport(); - tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary); + tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, validationSettings,true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary, validationSettings); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -2041,8 +2124,8 @@ public void testValidateFieldsOutOfOrderFIXT11() throws Exception { "627=2\u0001628=HOPID1\u0001629=20220414-15:22:54\u0001628=HOPID2\u0001629=20220414-15:22:54\u0001" + "10=129\u0001"; tcrUnOrdered = new TradeCaptureReport(); - tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary); + tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, validationSettings, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary, validationSettings); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -2063,8 +2146,9 @@ public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { + "54=2\u000137=OrderID2\u000111=7533509260093757876\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u0001" + "10=191\u0001"; final TradeCaptureReport tcrOrdered = new TradeCaptureReport(); - tcrOrdered.fromString(orderedData, sessDictionary, true); - DataDictionary.validate(tcrOrdered, sessDictionary, sessDictionary); + ValidationSettings validationSettings = new ValidationSettings(); + tcrOrdered.fromString(orderedData, sessDictionary, validationSettings, true); + DataDictionary.validate(tcrOrdered, sessDictionary, sessDictionary, validationSettings); // As this is our reference message created with all validations switched on, // make sure some message components @@ -2072,7 +2156,7 @@ public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { assertEquals(tcrOrdered.getHeader().getGroupCount(NoHops.FIELD), 2); assertEquals(tcrOrdered.getGroupCount(NoSides.FIELD), 2); - sessDictionary.setCheckFieldsOutOfOrder(false); + validationSettings.setCheckFieldsOutOfOrder(false); String unorderedData = "8=FIX.4.4\u00019=551\u000135=AE\u0001" + "22=4\u000131=27\u000132=5000.000000000000\u000148=AU000000ANZ3\u000155=ANZ\u000160=20220210-02:43:27.796\u000164=20220214\u000175=20220210\u0001106=4075\u0001167=CS\u0001461=Exxxxx\u0001487=0\u0001570=N\u0001571=TradeReportID\u0001762=1\u0001880=7533509260093686098:0#NORMAL#1644451200000000000\u0001" @@ -2084,8 +2168,8 @@ public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { + "34=545\u000149=SENDER\u000152=20220210-02:44:00.820\u000156=TARGET\u0001115=ON_BHEHALF\u0001" + "10=191\u0001"; TradeCaptureReport tcrUnOrdered = new TradeCaptureReport(); - tcrUnOrdered.fromString(unorderedData, sessDictionary, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary); + tcrUnOrdered.fromString(unorderedData, sessDictionary, validationSettings, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary, validationSettings); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -2100,8 +2184,8 @@ public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { + "10=191\u0001"; tcrUnOrdered = new TradeCaptureReport(); - tcrUnOrdered.fromString(unorderedData, sessDictionary, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary); + tcrUnOrdered.fromString(unorderedData, sessDictionary, validationSettings, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary, validationSettings); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -2118,8 +2202,8 @@ public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { + "627=2\u0001628=HOPID1\u0001629=20220414-15:22:54\u0001628=HOPID2\u0001629=20220414-15:22:54\u0001" + "10=191\u0001"; tcrUnOrdered = new TradeCaptureReport(); - tcrUnOrdered.fromString(unorderedData, sessDictionary, true); - DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary); + tcrUnOrdered.fromString(unorderedData, sessDictionary, validationSettings, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary, validationSettings); assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); @@ -2211,4 +2295,247 @@ private NewOrderSingle.NoAllocs setUpGroups(Message message) { message.addGroup(numAllocs); return numAllocs; } + + @Test + public void fromString_withNullSessionDictionary_shouldNotThrowNPE() throws InvalidMessage { + Message message = new Message(); + message.fromString("8=FIX.4.4|9=431|35=d|10=58|".replaceAll("\\|", "\001"), null, null, true); + } + + @Test + public void toString_withCanBeModified_false_should_not_add_body_length_or_checksum_fields() { + Message message = new Message(false); + message.setString(BeginString.FIELD, "FIX.4.4"); + message.setString(MsgType.FIELD, "D"); + assertEquals("8=FIX.4.4\u000135=D\u0001", message.toString()); + } + + @Test + public void toString_withCanBeModified_true_should_add_body_length_and_checksum_fields() { + Message message = new Message(); + message.setString(BeginString.FIELD, "FIX.4.4"); + message.setString(MsgType.FIELD, "D"); + assertEquals("9=15\u00018=FIX.4.4\u000135=D\u000110=232\u0001", message.toString()); + } + + @Test + public void testWeakParsingGeneratesSameMessage() throws Exception { + final quickfix.fix44.NewOrderSingle groupOrder = new quickfix.fix44.NewOrderSingle(); + final Group partyGroup = new Group(quickfix.field.NoPartyIDs.FIELD, PartyID.FIELD); + partyGroup.setField(new PartyID("Trader")); + partyGroup.setField(new PartyIDSource( + PartyIDSource.GENERALLY_ACCEPTED_MARKET_PARTICIPANT_IDENTIFIER)); + partyGroup.setField(new PartyRole(11)); + + final Group partyGroup2 = new Group(quickfix.field.NoPartyIDs.FIELD, PartyID.FIELD); + partyGroup2.setField(new PartyID("Trader2")); + partyGroup2.setField(new PartyIDSource( + PartyIDSource.GENERALLY_ACCEPTED_MARKET_PARTICIPANT_IDENTIFIER)); + partyGroup2.setField(new PartyRole(11)); + + groupOrder.addGroup(partyGroup); + groupOrder.addGroup(partyGroup2); + + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001448=Trader\001447=C\001452=11\001448=Trader2\001447=C\001452=11\00110=135\001", + true, + Message.WeakParsingMode.ENABLED); + assertNull(m.getException()); + assertEquals(groupOrder.toString(), m.toString()); + } + + @Test + public void testWeakParsingFallbackGeneratesSameMessage() throws Exception { + final quickfix.fix44.NewOrderSingle groupOrder = new quickfix.fix44.NewOrderSingle(); + final Group partyGroup = new Group(quickfix.field.NoPartyIDs.FIELD, PartyID.FIELD); + partyGroup.setField(new PartyID("Trader")); + partyGroup.setField(new PartyIDSource( + PartyIDSource.GENERALLY_ACCEPTED_MARKET_PARTICIPANT_IDENTIFIER)); + partyGroup.setField(new PartyRole(11)); + + final Group partyGroup2 = new Group(quickfix.field.NoPartyIDs.FIELD, PartyID.FIELD); + partyGroup2.setField(new PartyID("Trader2")); + partyGroup2.setField(new PartyIDSource( + PartyIDSource.GENERALLY_ACCEPTED_MARKET_PARTICIPANT_IDENTIFIER)); + partyGroup2.setField(new PartyRole(11)); + + groupOrder.addGroup(partyGroup); + groupOrder.addGroup(partyGroup2); + + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001448=Trader\001447=C\001452=11\001448=Trader2\001447=C\001452=11\00110=135\001", + true, + Message.WeakParsingMode.FALLBACK); + assertNull(m.getException()); + assertEquals(groupOrder.toString(), m.toString()); + } + + @Test + public void testWeakParsingDisabledStoresError() throws Exception { + final quickfix.fix44.NewOrderSingle groupOrder = new quickfix.fix44.NewOrderSingle(); + final Group partyGroup = new Group(quickfix.field.NoPartyIDs.FIELD, PartyID.FIELD); + partyGroup.setField(new PartyID("Trader")); + partyGroup.setField(new PartyIDSource( + PartyIDSource.GENERALLY_ACCEPTED_MARKET_PARTICIPANT_IDENTIFIER)); + partyGroup.setField(new PartyRole(11)); + + final Group partyGroup2 = new Group(quickfix.field.NoPartyIDs.FIELD, PartyID.FIELD); + partyGroup2.setField(new PartyID("Trader2")); + partyGroup2.setField(new PartyIDSource( + PartyIDSource.GENERALLY_ACCEPTED_MARKET_PARTICIPANT_IDENTIFIER)); + partyGroup2.setField(new PartyRole(11)); + + groupOrder.addGroup(partyGroup); + groupOrder.addGroup(partyGroup2); + + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001448=Trader\001447=C\001452=11\001448=Trader2\001447=C\001452=11\00110=135\001", + true, + Message.WeakParsingMode.DISABLED); + assertEquals("Tag appears more than once, field=448",m.getException().getMessage()); + } + + @Test + public void testWeakParsing_setsRemainingFields() throws Exception { + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001448=Trader\001447=C\001452=11\001448=Trader2\001447=C\001452=11\00110=135\001", + true, + Message.WeakParsingMode.ENABLED); + //Format: + //Header 8=FIX.4.49=10035=D + //Fields: 453=2448=Trader447=C452=11 + //Remaining Fields: 448=Trader2447=C452=11 + //Trailer: 10=135 + assertTrue(m.getHeader().isSetField(8)); + assertTrue(m.getHeader().isSetField(9)); + assertTrue(m.getHeader().isSetField(35)); + assertTrue(m.isSetField(453)); + assertTrue(m.isSetField(448)); + assertTrue(m.isSetField(447)); + assertTrue(m.isSetField(452)); + assertTrue(m.hasRemainingBodyFields()); + assertEquals(3, m.getRemainingBodyFields().size()); + assertEquals(448, m.getRemainingBodyFields().get(0).getTag()); + assertEquals("Trader2", m.getRemainingBodyFields().get(0).objectAsString()); + assertEquals(447, m.getRemainingBodyFields().get(1).getTag()); + assertEquals("C", m.getRemainingBodyFields().get(1).objectAsString()); + assertEquals(452, m.getRemainingBodyFields().get(2).getTag()); + assertEquals("11", m.getRemainingBodyFields().get(2).objectAsString()); + + assertEquals("Trader2", m.getFirstRemainingBodyFieldByTag(448).objectAsString()); + assertEquals("11", m.getRemainingBodyField(2).objectAsString()); + + assertTrue(m.getTrailer().isSetField(10)); + + } + + @Test + public void testWeakParsing_onlyPutsRepeatedTagAndSubsequentTagsInRemainingFields() throws Exception { + String messagePipe = "8=FIX.4.3|9=450|35=8|34=13674|49=SENDER|56=TARGET|52=20200801-12:01:45.018|" + + "37=ordId|11=clOrdId|17=execId|150=F|39=2|64=20240805|55=EUR/USD|461=RCSXXX|" + + "54=2|38=600000|40=D|44=1.07916|" + + "15=EUR|59=4|31=1.07916|32=600000|194=1.07916|151=0|14=600000|6=1.07916|" + + "124=2|" + + "79=acc1|17=exec2|32=300000|70=alloc1|" + + "79=acc2|17=exec3|32=300000|70=alloc2|" + + "10=252|"; + String messageSOH = messagePipe.replaceAll("\\|","\001"); + final Message m = new Message( + messageSOH, + true, + Message.WeakParsingMode.ENABLED); + assertTrue(m.isSetField(11)); + + assertEquals(m.getString(11), "clOrdId"); + assertTrue(m.hasRemainingBodyFields()); + assertEquals("acc2", m.getFirstRemainingBodyFieldByTag(79).objectAsString()); + assertEquals("exec2", m.getRemainingBodyField(0).objectAsString()); + } + + @Test + public void testWeakParsing_setsHeaderFields() throws Exception { + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001448=Trader\001447=C\001452=11\001448=Trader2\001447=C\001452=11\00110=135\001", + true, + Message.WeakParsingMode.ENABLED); + assertEquals("FIX.4.4", m.getHeader().getString(8)); + assertEquals("D", m.getHeader().getString(35)); + } + + @Test + public void testClone_copiesWeaklyParsedTags() throws Exception { + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001448=Trader\001447=C\001452=11\001448=Trader2\001447=C\001452=11\00110=135\001", + true, + Message.WeakParsingMode.ENABLED); + final Message m2 = (Message) m.clone(); + assertEquals(3, m2.getRemainingBodyFields().size()); + assertTrue(m2.hasRemainingBodyFields()); + } + @Test + public void testClone_copiesMessageData() throws Exception { + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001448=Trader\001447=C\001452=11\001448=Trader2\001447=C\001452=11\00110=135\001", + true); + final Message m2 = (Message) m.clone(); + assertEquals("8=FIX.4.4\0019=100\00135=D\001453=2\001448=Trader\001447=C\001452=11\001448=Trader2\001447=C\001452=11\00110=135\001", m2.toRawString()); + } + + @Test + public void testParsingWithUseFirstTagAsGroupDelimiterParsesMessage() throws Exception { + final ValidationSettings vs = new ValidationSettings(); + final DataDictionary dd = DataDictionaryTest.getDictionary("FIX44.xml"); + vs.setUseFirstTagAsGroupDelimiter(true); + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001447=B\001447=C\00110=094\001", + dd, vs, true); + assertNull(m.getException()); + assertEquals(2, m.getGroups(453).size()); + assertEquals("B",m.getGroups(453).get(0).getString(447)); + } + + @Test + public void test_fieldAtEndOfLastGroup_byDefault_isInGroup() throws Exception { + final ValidationSettings vs = new ValidationSettings(); + vs.setAllowUnknownMessageFields(true); + vs.setCheckUserDefinedFields(false); + final DataDictionary dd = DataDictionaryTest.getDictionary("FIX44.xml"); + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001448=B\001448=C\00110076=BIDS\00110=190\001", + dd, vs, true); + assertNull(m.getException()); + assertEquals(2, m.getGroups(453).size()); + assertEquals("BIDS",m.getGroups(453).get(1).getString(10076)); + } + + @Test + public void test_fieldAtEndOfLastGroup_withNotOnlyAllowSeen_isInGroup() throws Exception { + final ValidationSettings vs = new ValidationSettings(); + vs.setAllowUnknownMessageFields(true); + vs.setCheckUserDefinedFields(false); + vs.setOnlyAllowSeenOrKnownFieldsInLastGroup(false); + final DataDictionary dd = DataDictionaryTest.getDictionary("FIX44.xml"); + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001448=B\001448=C\00110076=BIDS\00110=190\001", + dd, vs, true); + assertNull(m.getException()); + assertEquals(2, m.getGroups(453).size()); + assertEquals("BIDS",m.getGroups(453).get(1).getString(10076)); + } + + @Test + public void test_fieldAtEndOfLastGroup_withOnlyAllowSeen_isNotInGroup() throws Exception { + final ValidationSettings vs = new ValidationSettings(); + vs.setAllowUnknownMessageFields(true); + vs.setCheckUserDefinedFields(false); + vs.setOnlyAllowSeenOrKnownFieldsInLastGroup(true); + final DataDictionary dd = DataDictionaryTest.getDictionary("FIX44.xml"); + final Message m = new Message( + "8=FIX.4.4\0019=100\00135=D\001453=2\001448=B\001448=C\00110076=BIDS\00110=190\001", + dd, vs, true); + assertNull(m.getException()); + assertEquals(2, m.getGroups(453).size()); + assertEquals("BIDS",m.getString(10076)); + } + } diff --git a/quickfixj-core/src/test/java/quickfix/MessageUtilsTest.java b/quickfixj-core/src/test/java/quickfix/MessageUtilsTest.java index 8038a219c0..30c08431b6 100644 --- a/quickfixj-core/src/test/java/quickfix/MessageUtilsTest.java +++ b/quickfixj-core/src/test/java/quickfix/MessageUtilsTest.java @@ -37,10 +37,10 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; + import org.junit.Test; + +import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -157,6 +157,7 @@ public void testGetStringFieldWithBadValue() throws Exception { public void testParse() throws Exception { Session mockSession = mock(Session.class); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); + when(mockSession.getWeakParsingMode()).thenReturn(Message.WeakParsingMode.DISABLED); when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); when(mockSession.getMessageFactory()).thenReturn(new quickfix.fix40.MessageFactory()); String messageString = "8=FIX.4.0\0019=56\00135=A\00134=1\00149=TW\001" + @@ -176,13 +177,14 @@ public void testLegacyParse() throws Exception { "44=15\00159=1\0016=0\001453=3\001448=AAA35791\001447=D\001452=3\001448=8\001" + "447=D\001452=4\001448=FIX11\001447=D\001452=36\00160=20060320-03:34:29\00110=169\001"; - Message message = MessageUtils.parse(new quickfix.fix40.MessageFactory(), DataDictionaryTest.getDictionary(), data); + Message message = MessageUtils.parse(new quickfix.fix40.MessageFactory(), DataDictionaryTest.getDictionary(), new ValidationSettings(), data); assertThat(message, is(notNullValue())); } @Test public void testParseFixt() throws Exception { Session mockSession = mock(Session.class); + when(mockSession.getWeakParsingMode()).thenReturn(Message.WeakParsingMode.DISABLED); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); when(mockSession.getMessageFactory()).thenReturn(new quickfix.fix40.MessageFactory()); @@ -201,6 +203,7 @@ public void testParseFixt() throws Exception { @Test public void testParseFixtLogon() throws Exception { Session mockSession = mock(Session.class); + when(mockSession.getWeakParsingMode()).thenReturn(Message.WeakParsingMode.DISABLED); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); when(mockSession.getMessageFactory()).thenReturn(new DefaultMessageFactory()); @@ -217,6 +220,7 @@ public void testParseFixtLogon() throws Exception { @Test public void testParseFixtLogout() throws Exception { Session mockSession = mock(Session.class); + when(mockSession.getWeakParsingMode()).thenReturn(Message.WeakParsingMode.DISABLED); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); when(mockSession.getMessageFactory()).thenReturn(new DefaultMessageFactory()); @@ -232,6 +236,7 @@ public void testParseFixtLogout() throws Exception { @Test public void testParseFix50() throws Exception { Session mockSession = mock(Session.class); + when(mockSession.getWeakParsingMode()).thenReturn(Message.WeakParsingMode.DISABLED); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); when(mockSession.getMessageFactory()).thenReturn(new DefaultMessageFactory()); @@ -252,6 +257,7 @@ public void testParseFix50() throws Exception { public void testParseMessageWithoutChecksumValidation() throws InvalidMessage { Session mockSession = mock(Session.class); when(mockSession.isValidateChecksum()).thenReturn(Boolean.FALSE); + when(mockSession.getWeakParsingMode()).thenReturn(Message.WeakParsingMode.DISABLED); DataDictionary dataDictionary = mock(DataDictionary.class); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); @@ -267,4 +273,30 @@ public void testParseMessageWithoutChecksumValidation() throws InvalidMessage { assertThat(message, is(notNullValue())); } + @Test + public void testParseUsingDictionaryBasedOnMessageTypes() throws InvalidMessage, FieldNotFound { + Session mockSession = mock(Session.class); + when(mockSession.isValidateChecksum()).thenReturn(Boolean.TRUE); + when(mockSession.getWeakParsingMode()).thenReturn(Message.WeakParsingMode.DISABLED); + when(mockSession.getValidationSettings()).thenReturn(new ValidationSettings()); + + when(mockSession.getDataDictionaryProvider()).thenReturn(new DefaultDataDictionaryProvider()); + when(mockSession.getMessageFactory()).thenReturn(new DefaultMessageFactory()); + + when(mockSession.useDictionaryForMsgType("D")).thenReturn(false); + + String messageString = "8=FIX.4.2\0019=56\00135=D\00134=1\00149=TW\00152=20060118-16:34:19\00156=ISLD\00178=1\00180=2\00110=283\001"; + + Message message = MessageUtils.parse(mockSession, messageString); + assertThat(message, is(notNullValue())); + assertEquals("2", message.getString(80)); + + when(mockSession.useDictionaryForMsgType("D")).thenReturn(true); + + Message messageWithDictionary = MessageUtils.parse(mockSession, messageString); + assertFalse(messageWithDictionary.hasValidStructure()); + assertEquals(80, messageWithDictionary.getException().getField()); + assertEquals("The group 78 must set the delimiter field 79, field=80", messageWithDictionary.getException().getMessage()); + } + } diff --git a/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java b/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java index b4c06797ae..bb46558dcd 100644 --- a/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java +++ b/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java @@ -183,9 +183,10 @@ public void onLogon(SessionID sessionId) { logonLatch.countDown(); } - public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, + @Override + public void fromAdmin(IMessage message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { - sessionMessages.put(sessionId, message); + sessionMessages.put(sessionId, (Message) message); if (messageLatch != null) { messageLatch.countDown(); } diff --git a/quickfixj-core/src/test/java/quickfix/ParsingAndValidationTest.java b/quickfixj-core/src/test/java/quickfix/ParsingAndValidationTest.java new file mode 100644 index 0000000000..b560e751c7 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/ParsingAndValidationTest.java @@ -0,0 +1,114 @@ +package quickfix; + +import org.junit.Test; +import quickfix.fix44.Logon; +import quickfix.fix44.Logout; + +import java.io.IOException; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static quickfix.SessionTestSupport.logonTo; + +public class ParsingAndValidationTest { + @Test + public void parseAndValidate_with_weakSessionParsing() + throws InvalidMessage, RejectLogon, UnsupportedMessageType, + IncorrectTagValue, FieldNotFound, IncorrectDataFormat, IOException { + String messageWithBars = "8=FIX.4.4|9=168|35=R|34=2|49=FXAMCOREQA2|56=RBOSRQ|52=20211008-13:24:55.066|131=18237830774659684985163369949506|146=1|55=EUR/USD|38=1500000.00|15=EUR|303=102|64=20211215|1=21339|63=B|10=107|"; + String messageString = messageWithBars.replaceAll("\\|","\001"); + final UnitTestApplication application = new UnitTestApplication(); + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + final Session session = new SessionFactoryTestSupport.Builder() + .setSessionId(sessionID).setApplication(application) + .setIsInitiator(false) + .setDataDictionaryProvider(null) + .setResetOnLogon(false) + .setValidateSequenceNumbers(true) + .setPersistMessages(true) + .setWeakParsingMode(Message.WeakParsingMode.ENABLED) + .build(); + + logonTo(session); + + Message message = MessageUtils.parse(session, messageString); + session.next(message); + assertNotNull(application.lastFromAppMessage()); + } + + @Test + public void parseAndValidate_with_weakSessionParsing_and_dictionary_passes_message_to_app() + throws InvalidMessage, RejectLogon, UnsupportedMessageType, + IncorrectTagValue, FieldNotFound, IncorrectDataFormat, IOException { + String messageWithBars = "8=FIX.4.4|9=168|35=R|34=2|49=FXAMCOREQA2|56=RBOSRQ|52=20211008-13:24:55.066|131=18237830774659684985163369949506|146=1|55=EUR/USD|38=1500000.00|15=EUR|303=102|64=20211215|1=21339|63=B|10=105|"; + String messageString = messageWithBars.replaceAll("\\|","\001"); + final UnitTestApplication application = new UnitTestApplication(); + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + final Session session = new SessionFactoryTestSupport.Builder() + .setSessionId(sessionID).setApplication(application) + .setIsInitiator(false) + .setDataDictionaryProvider(new DefaultDataDictionaryProvider()) + .setResetOnLogon(false) + .setValidateSequenceNumbers(true) + .setPersistMessages(true) + .setWeakParsingMode(Message.WeakParsingMode.ENABLED) + .build(); + + logonTo(session); + + Message message = MessageUtils.parse(session, messageString); + session.next(message); + assertNotNull(application.lastFromAppMessage()); + } + + @Test + public void parseAndValidate_with_weakSessionParsingDisabled_and_dictionary_does_not_pass_message_to_app() + throws InvalidMessage, RejectLogon, UnsupportedMessageType, + IncorrectTagValue, FieldNotFound, IncorrectDataFormat, IOException { + String messageWithBars = "8=FIX.4.4|9=168|35=R|34=2|49=FXAMCOREQA2|56=RBOSRQ|52=20211008-13:24:55.066|131=18237830774659684985163369949506|146=1|55=EUR/USD|38=1500000.00|15=EUR|303=102|64=20211215|1=21339|63=B|10=105|"; + String messageString = messageWithBars.replaceAll("\\|","\001"); + final UnitTestApplication application = new UnitTestApplication(); + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + final Session session = new SessionFactoryTestSupport.Builder() + .setSessionId(sessionID).setApplication(application) + .setIsInitiator(false) + .setDataDictionaryProvider(new DefaultDataDictionaryProvider()) + .setResetOnLogon(false) + .setValidateSequenceNumbers(true) + .setPersistMessages(true) + .setWeakParsingMode(Message.WeakParsingMode.DISABLED) + .build(); + + logonTo(session); + + Message message = MessageUtils.parse(session, messageString); + session.next(message); + assertNull(application.lastFromAppMessage()); + } + + @Test + public void parseAndValidate_with_weakSessionParsingFallback_and_dictionary_passes_message_to_app() + throws InvalidMessage, RejectLogon, UnsupportedMessageType, + IncorrectTagValue, FieldNotFound, IncorrectDataFormat, IOException { + String messageWithBars = "8=FIX.4.4|9=168|35=R|34=2|49=FXAMCOREQA2|56=RBOSRQ|52=20211008-13:24:55.066|131=18237830774659684985163369949506|146=1|55=EUR/USD|38=1500000.00|15=EUR|303=102|64=20211215|1=21339|63=B|10=105|"; + String messageString = messageWithBars.replaceAll("\\|","\001"); + final UnitTestApplication application = new UnitTestApplication(); + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + final Session session = new SessionFactoryTestSupport.Builder() + .setSessionId(sessionID).setApplication(application) + .setIsInitiator(false) + .setDataDictionaryProvider(new DefaultDataDictionaryProvider()) + .setResetOnLogon(false) + .setValidateSequenceNumbers(true) + .setPersistMessages(true) + .setWeakParsingMode(Message.WeakParsingMode.FALLBACK) + .build(); + + logonTo(session); + + Message message = MessageUtils.parse(session, messageString); + session.next(message); + assertNotNull(application.lastFromAppMessage()); + assertNull(application.lastFromAppMessage().getException()); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java b/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java index 1132a307f2..06c28f7c49 100644 --- a/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java +++ b/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java @@ -281,26 +281,28 @@ public void testSettingGettingGroupByReusingGroup() throws FieldNotFound { // Testing Message validation private static DataDictionary defaultDataDictionary = null; - private static DataDictionary defaultDataDictionaryWithIgnoreOutOfOrder = null; + private static ValidationSettings defaultDDSettings = null; + private static ValidationSettings ignoreOutOfOrderSettings = null; private static DataDictionary customDataDictionary = null; private final DefaultMessageFactory messageFactory = new DefaultMessageFactory(); static { try { - defaultDataDictionary = new DataDictionary("FIX44.xml"); - defaultDataDictionaryWithIgnoreOutOfOrder = new DataDictionary("FIX44.xml"); - defaultDataDictionaryWithIgnoreOutOfOrder.setCheckUnorderedGroupFields(false); - customDataDictionary = new DataDictionary("FIX44_Custom_Test.xml"); + defaultDataDictionary = new DataDictionary("FIX44.xml", true); + defaultDDSettings = new ValidationSettings(); + ignoreOutOfOrderSettings = new ValidationSettings(); + ignoreOutOfOrderSettings.setCheckUnorderedGroupFields(false); + customDataDictionary = new DataDictionary("FIX44_Custom_Test.xml", true); } catch (final ConfigError e) { e.printStackTrace(); } } - private Message buildValidatedMessage(String sourceFIXString, DataDictionary dd) + private Message buildValidatedMessage(String sourceFIXString, DataDictionary dd, ValidationSettings validationSettings) throws InvalidMessage { final Message message = messageFactory.create(MessageUtils.getStringField(sourceFIXString, BeginString.FIELD), MessageUtils.getMessageType(sourceFIXString)); - message.fromString(sourceFIXString, dd, true); + message.fromString(sourceFIXString, dd, validationSettings, true); return message; } @@ -321,7 +323,7 @@ public void testValidationWithNestedGroupAndStandardFields() throws InvalidMessa final String sourceFIXString = quoteRequest.toString(); final quickfix.fix44.QuoteRequest validatedMessage = (quickfix.fix44.QuoteRequest) buildValidatedMessage( - sourceFIXString, defaultDataDictionary); + sourceFIXString, defaultDataDictionary, defaultDDSettings); String validateFIXString = null; if (validatedMessage != null) { validateFIXString = validatedMessage.toString(); @@ -346,9 +348,9 @@ public void testValidationWithNestedGroupAndStandardFieldsFIX50SP2() throws Inva quoteRequest.addGroup(gNoRelatedSym); final String sourceFIXString = quoteRequest.toString(); - final DataDictionary fix50sp2DataDictionary = new DataDictionary("FIX50SP2.xml"); + final DataDictionary fix50sp2DataDictionary = new DataDictionary("FIX50SP2.xml", true); final quickfix.fix50sp2.QuoteRequest validatedMessage = (quickfix.fix50sp2.QuoteRequest) messageFactory.create(FixVersions.FIX50SP2, QuoteRequest.MSGTYPE); - validatedMessage.fromString(sourceFIXString, fix50sp2DataDictionary, true); + validatedMessage.fromString(sourceFIXString, fix50sp2DataDictionary, new ValidationSettings(), true); String validateFIXString = validatedMessage.toString(); @@ -371,8 +373,8 @@ public void testValidationWithNestedGroupAndStandardFieldsWithoutDelimiter() thr final String sourceFIXString = quoteRequest.toString(); - Message buildValidatedMessage = buildValidatedMessage(sourceFIXString, defaultDataDictionary); - assertEquals("The group 146 must set the delimiter field 55", buildValidatedMessage.getException().getMessage()); + Message buildValidatedMessage = buildValidatedMessage(sourceFIXString, defaultDataDictionary, defaultDDSettings); + assertEquals("The group 146 must set the delimiter field 55, field=555", buildValidatedMessage.getException().getMessage()); } @Test @@ -406,7 +408,7 @@ public void testGroupFieldsOrderWithCustomDataDictionary() throws InvalidMessage final String sourceFIXString = quoteRequest.toString(); final quickfix.fix44.QuoteRequest validatedMessage = (quickfix.fix44.QuoteRequest) buildValidatedMessage( - sourceFIXString, customDataDictionary); + sourceFIXString, customDataDictionary, new ValidationSettings()); assertNull("Invalid message", validatedMessage.getException()); @@ -421,9 +423,9 @@ public void testOutOfOrderGroupMembersDelimiterField() throws Exception { "8=FIX.4.4\0019=0\00135=D\00134=2\00149=TW\00152=