diff --git a/README.adoc b/README.adoc index 811a507..1277587 100644 --- a/README.adoc +++ b/README.adoc @@ -15,67 +15,6 @@ It simplifies the configuration required to create and start an https://www.quic In this Spring Boot Starter, QuickFIX/J Server is equivalent to `Acceptor` and QuickFIX/J Client to `Initiator`. -== Breaking changes with previous versions - -Due to the changes in https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x#core-container[how dependencies are autowired from Spring Framework 6.1] and https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes#parameter-name-discovery[Parameter Name Discovery], having the quickfixj client and server in the same spring context is not supported. - -If you have an application that need to use both, or multiple of one, you can use the following approach to separate the Spring contexts and configurations: - -[source,java] ----- -@SpringBootConfiguration -public class ApplicationConfiguration { - - public static void main(String[] args) { - SpringApplicationBuilder parent = - new SpringApplicationBuilder(QuickFixJCommonConfiguration.class); - parent.child(QuickFixJServerYourCompanyContextConfiguration.class).web(WebApplicationType.SERVLET); - parent.child(QuickFixJClientNDAQContextConfiguration.class).web(WebApplicationType.SERVLET); - parent.child(QuickFixJClientFOREXContextConfiguration.class).web(WebApplicationType.SERVLET); - parent.child(QuickFixJClientDOWJONESContextConfiguration.class).web(WebApplicationType.SERVLET); - parent.run(args); - } - - @Configuration - public class QuickFixJCommonConfiguration { - - @Bean - public QuickFixJTemplate quickFixJTemplate() { - return new QuickFixJTemplate(); - } - } - - - @EnableAutoConfiguration - @Configuration(proxyBeanMethods = false) - @PropertySource("classpath:server/server.properties") - public class QuickFixJServerYourCompanyContextConfiguration { - // override beans if required - } - - @EnableAutoConfiguration - @Configuration(proxyBeanMethods = false) - @PropertySource("classpath:client/ndaq.properties") - public class QuickFixJClientNDAQContextConfiguration { - // override beans if required - } - - @EnableAutoConfiguration - @Configuration(proxyBeanMethods = false) - @PropertySource("classpath:client/forex.properties") - public class QuickFixJClientFOREXContextConfiguration { - // override beans if required - } - - @EnableAutoConfiguration - @Configuration(proxyBeanMethods = false) - @PropertySource("classpath:client/dow-jones.properties") - public class QuickFixJClientDOWJONESContextConfiguration { - // override beans if required - } -} ----- - == Getting started To use the QuickFIX/J Server or QuickFIX/J Client you need to add the QuickFIX/J Spring Boot Starter dependency in your project. @@ -128,6 +67,72 @@ and add the specific version(s) of the FIX messages you need in your project `de ---- +== Breaking changes with previous versions + +Due to the changes in https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x#core-container[how dependencies are autowired from Spring Framework 6.1] and https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes#parameter-name-discovery[Parameter Name Discovery], having the quickfixj client and server in the same spring context is not supported. + +If you have an application that need to use both, or multiple of one, you can use the following approach to separate the Spring contexts and configurations: + +[source,java] +---- +public class ApplicationConfiguration { + + public static void main(String[] args) { + SpringApplicationBuilder parentBuilder + = new SpringApplicationBuilder(QuickFixJCommonConfiguration.class) + .web(WebApplicationType.NONE); + parentBuilder.run(args); + + parentBuilder.child(QuickFixJServerYourCompanyContextConfiguration.class) + .properties("spring.config.name=server") + .web(WebApplicationType.SERVLET).run(args); + parentBuilder.child(QuickFixJClientNDAQContextConfiguration.class) + .properties("spring.config.name=ndaq") + .web(WebApplicationType.SERVLET).run(args); + parentBuilder.child(QuickFixJClientFOREXContextConfiguration.class) + .properties("spring.config.name=forex") + .web(WebApplicationType.SERVLET).run(args); + parentBuilder.child(QuickFixJClientDOWJONESContextConfiguration.class) + .properties("spring.config.name=dow-jones") + .web(WebApplicationType.SERVLET).run(args); + } + + @Configuration + public class QuickFixJCommonConfiguration { + + @Bean + public QuickFixJTemplate quickFixJTemplate() { + return new QuickFixJTemplate(); + } + } + + + @EnableAutoConfiguration + @Configuration + public class QuickFixJServerYourCompanyContextConfiguration { + // override beans if required + } + + @EnableAutoConfiguration + @Configuration + public class QuickFixJClientNDAQContextConfiguration { + // override beans if required + } + + @EnableAutoConfiguration + @Configuration + public class QuickFixJClientFOREXContextConfiguration { + // override beans if required + } + + @EnableAutoConfiguration + @Configuration + public class QuickFixJClientDOWJONESContextConfiguration { + // override beans if required + } +} +---- + == QuickFIX/J Spring Boot Starter - Server (Acceptor) === Enabling QuickFix/J Server diff --git a/quickfixj-spring-boot-context/src/main/java/io/allune/quickfixj/spring/boot/starter/connection/SessionSettingsLocator.java b/quickfixj-spring-boot-context/src/main/java/io/allune/quickfixj/spring/boot/starter/connection/SessionSettingsLocator.java index 8a831ff..8105377 100644 --- a/quickfixj-spring-boot-context/src/main/java/io/allune/quickfixj/spring/boot/starter/connection/SessionSettingsLocator.java +++ b/quickfixj-spring-boot-context/src/main/java/io/allune/quickfixj/spring/boot/starter/connection/SessionSettingsLocator.java @@ -28,6 +28,7 @@ import java.util.Optional; import static java.util.Optional.empty; +import static org.apache.commons.lang3.StringUtils.isBlank; /** * {@link SessionSettings} helper class that attempts to load the settings files from the default locations @@ -66,6 +67,9 @@ public SessionSettings loadSettings(String... locations) { } public SessionSettings loadSettingsFromString(String configString) { + if (isBlank(configString)) { + throw new SettingsNotFoundException("configString is blank or empty"); + } try { return new SessionSettings(new ByteArrayInputStream(configString.getBytes())); } catch (RuntimeException | ConfigError e) { diff --git a/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/connection/ConnectorManagerTest.java b/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/connection/ConnectorManagerTest.java index 31ec3d6..33b7f1b 100644 --- a/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/connection/ConnectorManagerTest.java +++ b/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/connection/ConnectorManagerTest.java @@ -23,6 +23,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -116,4 +119,81 @@ public void shouldThrowConfigurationExceptionUponRuntimeErrorFailure() throws Ex verify(connector).start(); } + + @Test + void testIsAutoStartup_defaultTrue() { + Connector connector = mock(Connector.class); + ConnectorManager connectorManager = new ConnectorManager(connector); + // By default autoStartup should be true + assertTrue(connectorManager.isAutoStartup(), "The autoStartup should be true by default"); + } + + @Test + void testIsAutoStartup_setToFalse() { + Connector connector = mock(Connector.class); + ConnectorManager connectorManager = new ConnectorManager(connector); + // Set autoStartup to false + connectorManager.setAutoStartup(false); + assertFalse(connectorManager.isAutoStartup(), "The autoStartup should be false after setting it"); + } + + @Test + void testIsAutoStartup_setToTrue() { + Connector connector = mock(Connector.class); + ConnectorManager connectorManager = new ConnectorManager(connector); + + // First set it to false + connectorManager.setAutoStartup(false); + assertFalse(connectorManager.isAutoStartup(), "The autoStartup should be false after setting it"); + + // Then set it back to true + connectorManager.setAutoStartup(true); + assertTrue(connectorManager.isAutoStartup(), "The autoStartup should be true after setting it back"); + } + + @Test + void testGetPhase_defaultValue() { + Connector connector = mock(Connector.class); + ConnectorManager connectorManager = new ConnectorManager(connector); + // By default the phase should be Integer.MAX_VALUE + assertEquals(Integer.MAX_VALUE, connectorManager.getPhase(), "The default phase should be Integer.MAX_VALUE"); + } + + @Test + void testSetPhase_customValue() { + Connector connector = mock(Connector.class); + ConnectorManager connectorManager = new ConnectorManager(connector); + // Set phase to a specific value and check + int customPhase = 42; + connectorManager.setPhase(customPhase); + assertEquals(customPhase, connectorManager.getPhase(), "The phase should be the custom value set"); + } + + @Test + void testIsForceDisconnect_defaultValue() { + Connector connector = mock(Connector.class); + ConnectorManager connectorManager = new ConnectorManager(connector); + // By default, forceDisconnect should be false + assertFalse(connectorManager.isForceDisconnect(), "The default forceDisconnect should be false"); + } + + @Test + void testSetForceDisconnect_true() { + Connector connector = mock(Connector.class); + ConnectorManager connectorManager = new ConnectorManager(connector); + // Set forceDisconnect to true and check + connectorManager.setForceDisconnect(true); + assertTrue(connectorManager.isForceDisconnect(), "The forceDisconnect should be true after setting it"); + } + + @Test + void testSetForceDisconnect_false() { + Connector connector = mock(Connector.class); + ConnectorManager connectorManager = new ConnectorManager(connector); + // Initially set forceDisconnect to true + connectorManager.setForceDisconnect(true); + // Set forceDisconnect to false and check + connectorManager.setForceDisconnect(false); + assertFalse(connectorManager.isForceDisconnect(), "The forceDisconnect should be false after setting it back"); + } } diff --git a/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/connection/SessionSettingsLocatorTest.java b/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/connection/SessionSettingsLocatorTest.java index a2427ff..6f817d3 100644 --- a/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/connection/SessionSettingsLocatorTest.java +++ b/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/connection/SessionSettingsLocatorTest.java @@ -18,12 +18,18 @@ import io.allune.quickfixj.spring.boot.starter.exception.SettingsNotFoundException; import org.junit.jupiter.api.Test; import org.springframework.core.io.DefaultResourceLoader; +import quickfix.ConfigError; +import quickfix.FieldConvertError; +import quickfix.SessionID; import quickfix.SessionSettings; import java.io.File; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * @author Eduardo Sanchez-Ros @@ -50,4 +56,48 @@ public void shouldThrowSettingsNotFoundExceptionIfNoneFound() { assertThatThrownBy(() -> sessionSettingsLocator.loadSettings(null, null, null, null)) .isInstanceOf(SettingsNotFoundException.class); } + + @Test + void testLoadSettingsFromString_validConfig() throws ConfigError, FieldConvertError { + String configString = "[DEFAULT]\n" + + "ConnectionType=initiator\n" + + "BeginString=FIX.4.2\n" + + "SenderCompID=CLIENT1\n" + + "TargetCompID=SERVER\n" + + "\n" + + "[SESSION]\n" + + "SocketConnectPort=5001\n" + + "SocketConnectHost=localhost\n"; + + SessionSettingsLocator sessionSettingsLocator = new SessionSettingsLocator(new DefaultResourceLoader()); + SessionSettings settings = sessionSettingsLocator.loadSettingsFromString(configString); + + assertThat("initiator").isEqualTo(settings.getString("ConnectionType")); + assertThat("FIX.4.2").isEqualTo(settings.getString("BeginString")); + assertThat("CLIENT1").isEqualTo(settings.getString("SenderCompID")); + assertThat("SERVER").isEqualTo(settings.getString("TargetCompID")); + + SessionID sessionID = new SessionID("FIX.4.2", "CLIENT1", "SERVER"); + assertThat(5001).isEqualTo(settings.getLong(sessionID, "SocketConnectPort")); + assertThat("localhost").isEqualTo(settings.getString(sessionID, "SocketConnectHost")); + } + + @Test + void testLoadSettingsFromString_emptyConfig() { + String configString = ""; + SessionSettingsLocator sessionSettingsLocator = new SessionSettingsLocator(new DefaultResourceLoader()); + assertThatThrownBy(() -> sessionSettingsLocator.loadSettingsFromString(configString)) + .isInstanceOf(SettingsNotFoundException.class); + } + + @Test + void testLoadSettingsFromString_invalidConfig() { + String configString = "[INVALID]\n" + + "ThisIsNotAValidSetting"; + + SessionSettingsLocator sessionSettingsLocator = new SessionSettingsLocator(new DefaultResourceLoader()); + assertDoesNotThrow(() -> { + sessionSettingsLocator.loadSettingsFromString(configString); + }); + } } diff --git a/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/template/QuickFixJTemplateTest.java b/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/template/QuickFixJTemplateTest.java index 911873f..9894016 100644 --- a/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/template/QuickFixJTemplateTest.java +++ b/quickfixj-spring-boot-context/src/test/java/io/allune/quickfixj/spring/boot/starter/template/QuickFixJTemplateTest.java @@ -32,6 +32,7 @@ import quickfix.Message; import quickfix.Session; import quickfix.SessionID; +import quickfix.field.ApplVerID; import quickfix.field.BeginString; import quickfix.field.SenderCompID; import quickfix.field.TargetCompID; @@ -339,6 +340,28 @@ public void shouldNotThrowMessageValidationExceptionGivenValidationIsDisabled() verify(applicationDataDictionary, never()).validate(any()); } + @Test + public void shouldDoValidationForSpecificVersion() throws FieldNotFound, IncorrectTagValue, IncorrectDataFormat { + // Given + Message.Header header = mock(Message.Header.class); + given(message.getHeader()).willReturn(header); + given(header.getString(ApplVerID.FIELD)).willReturn(ApplVerID.FIX43); + given(header.getString(SenderCompID.FIELD)).willReturn(expectedSender); + given(header.getString(TargetCompID.FIELD)).willReturn(expectedTarget); + given(header.getString(BeginString.FIELD)).willReturn(expectedBeginString); + given(sessionLookupHandler.lookupBySessionID(any())).willReturn(session); + given(session.getDataDictionaryProvider()).willReturn(dataDictionaryProvider); + given(dataDictionaryProvider.getApplicationDataDictionary(new ApplVerID(ApplVerID.FIX43))).willReturn(applicationDataDictionary); + quickFixJTemplate.setDoValidation(true); + + // When/Then + assertThatCode(() -> quickFixJTemplate.send(message)).doesNotThrowAnyException(); + + verify(session).getDataDictionaryProvider(); + verify(dataDictionaryProvider).getApplicationDataDictionary(any()); + verify(applicationDataDictionary).validate(any(), any(Boolean.class)); + } + private void assertSessionID(SessionID expectedSessionID) { ArgumentCaptor sessionIDCaptor = ArgumentCaptor.forClass(SessionID.class); verify(sessionLookupHandler).lookupBySessionID(sessionIDCaptor.capture());