diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..030dd5522 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: java +sudo: false +jdk: + - oraclejdk8 +before_script: + - ulimit -n 4096 +before_install: + - echo "MAVEN_OPTS='-Xms2g -Xmx3g'" > ~/.mavenrc +script: + - travis_wait 30 mvn test -B -V -Djava.util.logging.config.file=logging.properties diff --git a/README.md b/README.md index 637b8861d..a82213811 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ QuickFIX/J ========== +[![Build Status](https://travis-ci.org/quickfix-j/quickfixj.svg?branch=master)](https://travis-ci.org/quickfix-j/quickfixj) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.quickfixj/quickfixj-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.quickfixj/quickfixj-core) + This is the official QuickFIX/J project repository. The Financial Information eXchange (FIX) protocol is a messaging standard developed diff --git a/pom.xml b/pom.xml index 39b8f1072..e5f578a5e 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.quickfixj quickfixj-parent - 1.7.0-SNAPSHOT + 2.1.0-SNAPSHOT pom QuickFIX/J Parent @@ -52,7 +52,7 @@ - 3.3.9 + 3.5.0 @@ -68,25 +68,25 @@ UTF-8 UTF-8 - 1.7 - 1.7.21 + 1.8 + 1.7.25 4.12 - + - 3.0.1 - 3.5.1 + 3.0.2 + 3.7.0 3.0.2 - 2.19.1 - 3.7 + 2.20.1 + 3.8 3.0.1 2.10.4 - 2.4.3 - 2.6 - 3.2.0 + 3.1.0 + 3.1.0 + 3.3.0 1.6 2.8.2 - 1.6.7 - 1.12 + 1.6.8 + 3.0.0 @@ -106,9 +106,12 @@ org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin-version} + true ${jdkLevel} ${jdkLevel} + 2g + 4g @@ -155,9 +158,6 @@ org.apache.maven.plugins maven-javadoc-plugin ${maven-javadoc-plugin-version} - - quickfix.fix* - attach-javadocs @@ -166,6 +166,7 @@ -Xdoclint:none + 3g diff --git a/quickfixj-all/pom.xml b/quickfixj-all/pom.xml index 28fb8e9c2..2347ca65a 100644 --- a/quickfixj-all/pom.xml +++ b/quickfixj-all/pom.xml @@ -4,7 +4,7 @@ org.quickfixj quickfixj-parent - 1.7.0-SNAPSHOT + 2.1.0-SNAPSHOT quickfixj-all diff --git a/quickfixj-codegenerator/pom.xml b/quickfixj-codegenerator/pom.xml index d3941d30c..07938f278 100644 --- a/quickfixj-codegenerator/pom.xml +++ b/quickfixj-codegenerator/pom.xml @@ -4,7 +4,7 @@ org.quickfixj quickfixj-parent - 1.7.0-SNAPSHOT + 2.1.0-SNAPSHOT quickfixj-codegenerator @@ -25,13 +25,18 @@ org.apache.maven maven-plugin-api - 3.3.9 + 3.5.0 org.apache.maven maven-project 2.2.1 + + net.sf.saxon + Saxon-HE + 9.8.0-4 + diff --git a/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/GenerateMojo.java b/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/GenerateMojo.java index 6788cf195..d39c0234d 100644 --- a/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/GenerateMojo.java +++ b/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/GenerateMojo.java @@ -19,12 +19,13 @@ package org.quickfixj.codegenerator; -import java.io.File; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.FileUtils; +import java.io.File; + /** * A mojo that uses the quickfix code generator to generate * Java source files from a QuickFIX Dictionary. @@ -137,8 +138,6 @@ public void execute() throws MojoExecutionException { task.setOrderedFields(orderedFields); task.setDecimalGenerated(decimal); generator.generate(task); - } catch (Exception e) { - throw new MojoExecutionException("QuickFIX code generator execution failed", e); } catch (Throwable t) { throw new MojoExecutionException("QuickFIX code generator execution failed", t); } diff --git a/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/MessageCodeGenerator.java b/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/MessageCodeGenerator.java index d6787fde6..44d0b00cd 100644 --- a/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/MessageCodeGenerator.java +++ b/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/MessageCodeGenerator.java @@ -19,17 +19,10 @@ package org.quickfixj.codegenerator; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -42,11 +35,17 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Generates Message and Field related code for the various FIX versions. @@ -84,7 +83,7 @@ private void generateMessageBaseClass(Task task) throws ParserConfigurationException, SAXException, IOException, TransformerFactoryConfigurationError, TransformerException { logInfo(task.getName() + ": generating message base class"); - Map parameters = new HashMap(); + Map parameters = new HashMap<>(); parameters.put(XSLPARAM_SERIAL_UID, SERIAL_UID_STR); generateClassCode(task, "Message", parameters); } @@ -107,7 +106,7 @@ private void generateClassCode(Task task, String className, Map TransformerException { logDebug("generating " + className + " for " + task.getName()); if (parameters == null) { - parameters = new HashMap(); + parameters = new HashMap<>(); } parameters.put("messagePackage", task.getMessagePackage()); parameters.put("fieldPackage", task.getFieldPackage()); @@ -131,7 +130,7 @@ private void generateFieldClasses(Task task) throws ParserConfigurationException String outputFile = outputDirectory + fieldName + ".java"; if (!new File(outputFile).exists()) { logDebug("field: " + fieldName); - Map parameters = new HashMap(); + Map parameters = new HashMap<>(); parameters.put("fieldName", fieldName); parameters.put("fieldPackage", task.getFieldPackage()); if (task.isDecimalGenerated()) { @@ -159,7 +158,7 @@ private void generateMessageSubclasses(Task task) throws ParserConfigurationExce Transformer transformer = createTransformer(task, "MessageSubclass.xsl"); for (String messageName : messageNames) { logDebug("generating message class: " + messageName); - Map parameters = new HashMap(); + Map parameters = new HashMap<>(); parameters.put("itemName", messageName); parameters.put(XSLPARAM_SERIAL_UID, SERIAL_UID_STR); parameters.put("orderedFields", Boolean.toString(task.isOrderedFields())); @@ -185,7 +184,7 @@ private void generateComponentClasses(Task task) throws ParserConfigurationExcep Transformer transformer = createTransformer(task, "MessageSubclass.xsl"); for (String componentName : componentNames) { logDebug("generating component class: " + componentName); - Map parameters = new HashMap(); + Map parameters = new HashMap<>(); parameters.put("itemName", componentName); parameters.put("baseClass", "quickfix.MessageComponent"); parameters.put("subpackage", ".component"); @@ -208,11 +207,11 @@ private Transformer createTransformer(Task task, String xsltFile) logInfo("Loading predefined xslt file:" + xsltFile); styleSource = new StreamSource(this.getClass().getResourceAsStream(xsltFile)); } - TransformerFactory transformerFactory = TransformerFactory.newInstance(); + TransformerFactory transformerFactory = new net.sf.saxon.TransformerFactoryImpl(); return transformerFactory.newTransformer(styleSource); } - private final Map specificationCache = new HashMap(); + private final Map specificationCache = new HashMap<>(); private Document getSpecification(Task task) throws ParserConfigurationException, SAXException, IOException { @@ -242,7 +241,7 @@ private void writePackageDocumentation(String outputDirectory, String descriptio } private List getNames(Element element, String path) { - return getNames(element, path, new ArrayList()); + return getNames(element, path, new ArrayList<>()); } private List getNames(Element element, String path, List names) { diff --git a/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/Fields.xsl b/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/Fields.xsl index bdd75a99c..1f2228fb5 100644 --- a/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/Fields.xsl +++ b/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/Fields.xsl @@ -18,7 +18,7 @@ ***************************************************************************** --> - + @@ -60,8 +60,12 @@ package ; import quickfix.Field; - -import java.util.Date; + +import java.time.LocalDateTime; + +import java.time.LocalDate; + +import java.time.LocalTime; public class extends Field { @@ -95,10 +99,10 @@ public class extends String - Date - Date - Date - Date + LocalDateTime + LocalTime + LocalDate + LocalDate boolean double @@ -147,26 +151,39 @@ public class extends + + + N_ + + .,+-=:()/&"'<> + + + + + + + - public static final String = ""; + public static final String = ""; - public static final String = ""; + public static final String = ""; - public static final String = ""; + public static final String = ""; - public static final boolean = ; + public static final boolean = ; - public static final int = ; + public static final int = ; - public static final int = ; + public static final int = ; - public static final String = ""; + public static final String = ""; - public static final String = ""; + public static final String = ""; - public static final char = ''; + public static final char = ''; diff --git a/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/Message.xsl b/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/Message.xsl index bd605b2fd..defc0a521 100644 --- a/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/Message.xsl +++ b/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/Message.xsl @@ -18,7 +18,7 @@ ***************************************************************************** --> - + diff --git a/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/MessageFactory.xsl b/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/MessageFactory.xsl index ad15616e9..50a541e2d 100644 --- a/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/MessageFactory.xsl +++ b/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/MessageFactory.xsl @@ -18,7 +18,7 @@ ***************************************************************************** --> - + diff --git a/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/MessageSubclass.xsl b/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/MessageSubclass.xsl index 6eec7068d..4333c3b62 100644 --- a/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/MessageSubclass.xsl +++ b/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/MessageSubclass.xsl @@ -18,7 +18,7 @@ ***************************************************************************** --> - + @@ -191,7 +191,10 @@ import quickfix.Group; - + + + + diff --git a/quickfixj-core/pom.xml b/quickfixj-core/pom.xml index e835754fe..7874eace0 100644 --- a/quickfixj-core/pom.xml +++ b/quickfixj-core/pom.xml @@ -1,423 +1,440 @@ - 4.0.0 - - org.quickfixj - quickfixj-parent - 1.7.0-SNAPSHOT - + 4.0.0 + + org.quickfixj + quickfixj-parent + 2.1.0-SNAPSHOT + - quickfixj-core - bundle + quickfixj-core + bundle - QuickFIX/J Core engine - The core QuickFIX/J engine - http://www.quickfixj.org + QuickFIX/J Core engine + The core QuickFIX/J engine + http://www.quickfixj.org - - **/AcceptanceTestSuite.java - org.quickfixj.Version - + + **/AcceptanceTestSuite.java + org.quickfixj.Version + - - - junit - junit - ${junit.version} - test - - - org.mockito - mockito-all - 1.10.19 - test - - - org.hamcrest - hamcrest-all - 1.1 - test - - - hsqldb - hsqldb - 1.8.0.10 - test - - - tyrex - tyrex - 1.0.1 - test - - - org.slf4j - slf4j-jdk14 - ${slf4j.version} - test - + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-all + 1.10.19 + test + + + org.hamcrest + hamcrest-all + 1.1 + test + + + hsqldb + hsqldb + 1.8.0.10 + test + + + tyrex + tyrex + 1.0.1 + test + + + org.slf4j + slf4j-jdk14 + ${slf4j.version} + test + - - org.apache.mina - mina-core - 2.0.15 - - - org.slf4j - slf4j-api - ${slf4j.version} - + + org.apache.mina + mina-core + 2.0.17 + + + org.slf4j + slf4j-api + ${slf4j.version} + - - com.cloudhopper.proxool - proxool - 0.9.1 - true - - - - avalon-framework - avalon-framework-api - - - - commons-logging - commons-logging - - - - - com.cloudhopper.proxool - proxool-cglib - 0.9.1 - true - - - - avalon-framework - avalon-framework-api - - - - commons-logging - commons-logging - - - - - - org.slf4j - jcl-over-slf4j - ${slf4j.version} - runtime - true - - - berkeleydb - je - 2.1.30 - true - - + + com.cloudhopper.proxool + proxool + 0.9.1 + true + + + + avalon-framework + avalon-framework-api + + + + commons-logging + commons-logging + + + + + com.cloudhopper.proxool + proxool-cglib + 0.9.1 + true + + + + avalon-framework + avalon-framework-api + + + + commons-logging + commons-logging + + + + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + runtime + true + + + berkeleydb + je + 2.1.30 + true + + - - - - ../quickfixj-messages/quickfixj-messages-fixt11/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix50/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix44/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix43/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix42/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix41/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix40/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix50sp1/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix50sp2/src/main/resources - - + + + + ../quickfixj-messages/quickfixj-messages-fixt11/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix50/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix44/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix43/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix42/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix41/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix40/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix50sp1/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix50sp2/src/main/resources + + - - - src/test/resources - - - ../quickfixj-messages/quickfixj-messages-fixt11/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix50sp2/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix50sp1/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix50/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix44/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix43/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix42/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix41/src/main/resources - - - ../quickfixj-messages/quickfixj-messages-fix40/src/main/resources - - - src/main/resources - - + + + src/test/resources + + + ../quickfixj-messages/quickfixj-messages-fixt11/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix50sp2/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix50sp1/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix50/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix44/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix43/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix42/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix41/src/main/resources + + + ../quickfixj-messages/quickfixj-messages-fix40/src/main/resources + + + src/main/resources + + - - - org.quickfixj - quickfixj-codegenerator - ${project.version} - - - fixt11 - - generate - - - ../quickfixj-messages/quickfixj-messages-fixt11/src/main/resources/FIXT11.xml - quickfix.fixt11 - quickfix.field - ${generator.decimal} - - - - fix50 - - generate - - - ../quickfixj-messages/quickfixj-messages-fix50/src/main/resources/FIX50.xml - quickfix.fix50 - quickfix.field - ${generator.decimal} - - - - fix44 - - generate - - - ../quickfixj-messages/quickfixj-messages-fix44/src/main/resources/FIX44.modified.xml - quickfix.fix44 - quickfix.field - ${generator.decimal} - - - - fix43 - - generate - - - ../quickfixj-messages/quickfixj-messages-fix43/src/main/resources/FIX43.xml - quickfix.fix43 - quickfix.field - ${generator.decimal} - - - - fix42 - - generate - - - ../quickfixj-messages/quickfixj-messages-fix42/src/main/resources/FIX42.xml - quickfix.fix42 - quickfix.field - ${generator.decimal} - - - - fix41 - - generate - - - ../quickfixj-messages/quickfixj-messages-fix41/src/main/resources/FIX41.xml - quickfix.fix41 - quickfix.field - ${generator.decimal} - - - - fix40 - - generate - - - ../quickfixj-messages/quickfixj-messages-fix40/src/main/resources/FIX40.xml - quickfix.fix40 - quickfix.field - ${generator.decimal} - - - - fix50sp2 - - generate - - - ../quickfixj-messages/quickfixj-messages-fix50sp2/src/main/resources/FIX50SP2.modified.xml - quickfix.fix50sp2 - quickfix.field - ${generator.decimal} - - - - fix50sp1 - - generate - - - ../quickfixj-messages/quickfixj-messages-fix50sp1/src/main/resources/FIX50SP1.modified.xml - quickfix.fix50sp1 - quickfix.field - ${generator.decimal} - - - - - - org.apache.maven.plugins - maven-jar-plugin - ${maven-jar-plugin-version} - - - quickfix/** - org/** - quickfix/field/converter/* - FIX*.xml - - - quickfix/field/* - quickfix/fix*/** - - - - - org.apache.maven.plugins - maven-source-plugin - - - quickfix/** - org/** - quickfix/field/converter/* - FIX*.xml - - - quickfix/field/* - quickfix/fix*/** - - - - - org.apache.felix - maven-bundle-plugin - - - quickfix,quickfix.field.*,quickfix.mina.*,org.quickfixj.* - - - quickfix.fix40;resolution:=optional, - quickfix.fix41;resolution:=optional, - quickfix.fix42;resolution:=optional, - quickfix.fix43;resolution:=optional, - quickfix.fix44;resolution:=optional, - quickfix.fix50;resolution:=optional, - quickfix.fix50sp1;resolution:=optional, - quickfix.fix50sp2;resolution:=optional, - quickfix.fixt11;resolution:=optional, - - quickfix,quickfix.field,* - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - -Xmx512m -XX:MaxPermSize=256m - - **/*Test.java - ${acceptance.tests} - - - **/*ForTest.java - **/Abstract*Test.java - **/AcceptanceTestSuite$* - - - 5 - 60000 - 5 - false - - - - - + + + org.quickfixj + quickfixj-codegenerator + ${project.version} + + + fixt11 + + generate + + + ../quickfixj-messages/quickfixj-messages-fixt11/src/main/resources/FIXT11.xml + quickfix.fixt11 + quickfix.field + ${generator.decimal} + + + + fix50 + + generate + + + ../quickfixj-messages/quickfixj-messages-fix50/src/main/resources/FIX50.xml + quickfix.fix50 + quickfix.field + ${generator.decimal} + + + + fix44 + + generate + + + ../quickfixj-messages/quickfixj-messages-fix44/src/main/resources/FIX44.modified.xml + quickfix.fix44 + quickfix.field + ${generator.decimal} + + + + fix43 + + generate + + + ../quickfixj-messages/quickfixj-messages-fix43/src/main/resources/FIX43.xml + quickfix.fix43 + quickfix.field + ${generator.decimal} + + + + fix42 + + generate + + + ../quickfixj-messages/quickfixj-messages-fix42/src/main/resources/FIX42.xml + quickfix.fix42 + quickfix.field + ${generator.decimal} + + + + fix41 + + generate + + + ../quickfixj-messages/quickfixj-messages-fix41/src/main/resources/FIX41.xml + quickfix.fix41 + quickfix.field + ${generator.decimal} + + + + fix40 + + generate + + + ../quickfixj-messages/quickfixj-messages-fix40/src/main/resources/FIX40.xml + quickfix.fix40 + quickfix.field + ${generator.decimal} + + + + fix50sp1 + + generate + + + ../quickfixj-messages/quickfixj-messages-fix50sp1/src/main/resources/FIX50SP1.modified.xml + quickfix.fix50sp1 + quickfix.field + ${generator.decimal} + + + + fix50sp2 + + generate + + + ../quickfixj-messages/quickfixj-messages-fix50sp2/src/main/resources/FIX50SP2.modified.xml + quickfix.fix50sp2 + quickfix.field + ${generator.decimal} + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin-version} + + + quickfix/** + org/** + quickfix/field/converter/* + FIX*.xml + + + quickfix/field/* + quickfix/fix*/** + + + + + org.apache.maven.plugins + maven-source-plugin + + + quickfix/** + org/** + quickfix/field/converter/* + FIX*.xml + + + quickfix/field/* + quickfix/fix*/** + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin-version} + + + attach-javadocs + + jar + + + -Xdoclint:none + 3g + false + + src/main/java + + + + + + org.apache.felix + maven-bundle-plugin + + + quickfix,quickfix.field.*,quickfix.mina.*,org.quickfixj.* + + + quickfix.fix40;resolution:=optional, + quickfix.fix41;resolution:=optional, + quickfix.fix42;resolution:=optional, + quickfix.fix43;resolution:=optional, + quickfix.fix44;resolution:=optional, + quickfix.fix50;resolution:=optional, + quickfix.fix50sp1;resolution:=optional, + quickfix.fix50sp2;resolution:=optional, + quickfix.fixt11;resolution:=optional, + + quickfix,quickfix.field,* + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Xmx512m -Djava.net.preferIPv4Stack=true + false + + **/*Test.java + ${acceptance.tests} + + + **/*ForTest.java + **/Abstract*Test.java + **/AcceptanceTestSuite$* + + + 5 + 60000 + 5 + false + + + + + - - - - maven-surefire-report-plugin - ${maven-surefire-plugin-version} - - - true - - - - maven-javadoc-plugin - ${maven-javadoc-plugin-version} - - - maven-jxr-plugin - 2.5 - - - + + + + maven-surefire-report-plugin + ${maven-surefire-plugin-version} + + + true + + + + maven-jxr-plugin + 2.5 + + + - - - - skipAT - - - skipAT - true - - - - - - - + + + + skipAT + + + skipAT + true + + + + + + + diff --git a/quickfixj-core/src/main/doc/usermanual/index.html b/quickfixj-core/src/main/doc/usermanual/index.html index 87edb6c82..81e10dee7 100644 --- a/quickfixj-core/src/main/doc/usermanual/index.html +++ b/quickfixj-core/src/main/doc/usermanual/index.html @@ -32,7 +32,7 @@

About QuickFIX/J...

  • Free! It costs nothing and has a very liberal open source licence.
  • Full source code available (also at no cost).
  • Supports FIX versions 4.0 - 5.0SP2.
  • -
  • Runs on any hardware and operating system supported by 1.7+ Java SE or compatible VM.
  • +
  • Runs on any hardware and operating system supported by 1.8+ Java SE or compatible VM.
  • Compatibility with QuickFIX C++ Java Native Wrapper API (easy to upgrade)
  • Java NIO asynchronous network communications for scalability (using Apache MINA)
  • Easy to embed in existing Java applications.
  • diff --git a/quickfixj-core/src/main/doc/usermanual/installation.html b/quickfixj-core/src/main/doc/usermanual/installation.html index e0d32c598..500f09e8d 100644 --- a/quickfixj-core/src/main/doc/usermanual/installation.html +++ b/quickfixj-core/src/main/doc/usermanual/installation.html @@ -56,16 +56,12 @@

    Required run-time libraries:

    Description - mina-core-2.0.9.jar + mina-core-2.0.16.jar Socket handling (Java NIO) slf4j-api.jar - SLF4J library for JDK 1.4+ logging. - - - slf4j-jdk14.jar - SLF4J library for JDK 1.4+ logging. + SLF4J library for JDK logging. @@ -166,75 +162,56 @@

    IDE support:

    Maven Integration:

    If you are using the Maven build system, you can reference - the pre-built QuickFIX/J libraries hosted at the Marketcetera + the pre-built QuickFIX/J libraries hosted at the Central Repository repository.

    -
      -
    1. Add the following to your dependencies section, with appropriate modifications based on +

      Add the following to your dependencies section, with appropriate modifications based on the logging subsystem you choose:

       <!-- QuickFIX/J dependencies -->
       <dependency>
      -    <groupId>quickfixj</groupId>
      +    <groupId>org.quickfixj</groupId>
           <artifactId>quickfixj-core</artifactId>
      -    <version>1.6.0</version>
      -</dependency>
      -<dependency>
      -    <groupId>quickfixj</groupId>
      -    <artifactId>quickfixj-msg-fix40</artifactId>
      -    <version>1.6.0</version>
      +    <version>2.0.0</version>
       </dependency>
       <dependency>
      -    <groupId>quickfixj</groupId>
      -    <artifactId>quickfixj-msg-fix41</artifactId>
      -    <version>1.6.0</version>
      +    <groupId>org.quickfixj</groupId>
      +    <artifactId>quickfixj-messages-fix40</artifactId>
      +    <version>2.0.0</version>
       </dependency>
       <dependency>
      -    <groupId>quickfixj</groupId>
      -    <artifactId>quickfixj-msg-fix42</artifactId>
      -    <version>1.6.0</version>
      +    <groupId>org.quickfixj</groupId>
      +    <artifactId>quickfixj-messages-fix41</artifactId>
      +    <version>2.0.0</version>
       </dependency>
       <dependency>
      -    <groupId>quickfixj</groupId>
      -    <artifactId>quickfixj-msg-fix43</artifactId>
      -    <version>1.6.0</version>
      +    <groupId>org.quickfixj</groupId>
      +    <artifactId>quickfixj-messages-fix42</artifactId>
      +    <version>2.0.0</version>
       </dependency>
       <dependency>
      -    <groupId>quickfixj</groupId>
      -    <artifactId>quickfixj-msg-fix44</artifactId>
      -    <version>1.6.0</version>
      +    <groupId>org.quickfixj</groupId>
      +    <artifactId>quickfixj-messages-fix43</artifactId>
      +    <version>2.0.0</version>
       </dependency>
       <dependency>
      -    <groupId>org.apache.mina</groupId>
      -    <artifactId>mina-core</artifactId>
      -    <version>2.0.9</version>
      +    <groupId>org.quickfixj</groupId>
      +    <artifactId>quickfixj-messages-fix44</artifactId>
      +    <version>2.0.0</version>
       </dependency>
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-log4j12</artifactId>
      -    <version>1.7.12</version>
      +    <version>1.7.22</version>
       </dependency>
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-api</artifactId>
      -    <version>1.7.12</version>
      +    <version>1.7.22</version>
       </dependency>
      -
    2. -
    3. And add the Marketcetera Repository to your repository list:
      -
      -<repositories>
      -    <repository>
      -        <id>MarketceteraRepo</id>
      -        <url>http://repo.marketcetera.org/maven</url>
      -            <releases>
      -                <enabled>true</enabled>
      -            </releases>
      -    </repository>
      -</repositories>
      -
    4. -
    -There's an example POM file if you need it. +

    +

    Generating the database for JDBC based store and log

    -

    Everything needed to generate your database is in the etc/sql subdirectories. +

    Everything needed to generate your database is in the src/main/resources/config/sql subdirectories. For MySQL, there are the script and batch files create_mysql.sh and create_mysql.bat. These scripts will work on a newly installed mysql database with default permisions. The scripts will try to generate the database using the root MySQL account diff --git a/quickfixj-core/src/main/doc/usermanual/usage/configuration.html b/quickfixj-core/src/main/doc/usermanual/usage/configuration.html index e735cf3ac..9259dbf3c 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/configuration.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/configuration.html @@ -121,21 +121,21 @@

    QuickFIX Settings

    TargetCompID - (Optional) counterparty's compID as associated with this FIX session + Counterparty's compID as associated with this FIX session case-sensitive alpha-numeric string   TargetSubID - (Optional) counterparty's subID as associated with this FIX session + (Optional) Counterparty's subID as associated with this FIX session case-sensitive alpha-numeric string   TargetLocationID - (Optional) counterparty's locationID as associated with this FIX session + (Optional) Counterparty's locationID as associated with this FIX session case-sensitive alpha-numeric string   @@ -202,18 +202,32 @@

    QuickFIX Settings

    StartDay - For week long sessions, the starting day of week for the session. Use in combination with StartTime. + For week long sessions, the starting day of week for the session. +
    Use in combination with StartTime. +
    Incompatible with Weekdays Day of week in the default locale (e.g. Monday, mon, lundi, lun. etc.)   EndDay - For week long sessions, the ending day of week for the session. Use in combination with EndTime. + For week long sessions, the ending day of week for the session. +
    Use in combination with EndTime. +
    Incompatible with Weekdays Day of week in the default locale (e.g. Monday, mon, lundi, lun. etc.)   + + Weekdays + For daily sessions that are active on specific days of the week. +
    Use in combination with StartTime and EndTime. +
    Incompatible with StartDay and EndDay. +
    If StartTime is before EndTime then the day corresponds to the StartTime. + Comma-delimited list of days of the week in the default locale (e.g. "Sun,Mon,Tue", "Dimanche,Lundi,Mardi" etc.) + +   + NonStopSession If set the session will never reset. This is effectively the same as setting 00:00:00 as StartTime and EndTime. @@ -221,12 +235,19 @@

    QuickFIX Settings

    N - MillisecondsInTimeStamp - Determines if milliseconds should be added to - timestamps. Only available for FIX.4.2 and greater. - Y
    N + TimeStampPrecision + Determines precision for timestamps in (Orig)SendingTime fields. + Only available for FIX.4.2 and greater.

    + NB: This configuration is only considered for messages that are sent out. QuickFIX/J is able to receive UtcTimestamp fields with up to picosecond precision.
    + Please note however that only up to nanosecond precision will be stored, i.e. the picoseconds will be truncated. + + One of +
    • SECONDS
    • +
    • MILLIS
    • +
    • MICROS
    • +
    • NANOS
    - Y + MILLIS ClosedResendInterval @@ -476,12 +497,30 @@

    QuickFIX Settings

    "TCP" or "VM_PIPE". "TCP" + + SocketConnectPort<n> + Alternate socket port(s) for connecting to a session for failover or load balancing, + where n is a positive integer, i.e. SocketConnectPort1, SocketConnectPort2, etc. + Must be consecutive and have a matching SocketConnectHost<n> + + positive integer +   + SocketConnectHost<n> - Alternate socket hosts for connecting to a session for failover, where n is a - positive integer. (i.e.) SocketConnectHost1, SocketConnectHost2... must be consecutive - and have a matching SocketConnectPort[n] + Alternate socket host(s) for connecting to a session for failover or load balancing, + where n is a positive integer, i.e. SocketConnectHost1, SocketConnectHost2, etc. + Must be consecutive and have a matching SocketConnectPort<n> +

    + Connection list iteration rules: +

      +
    • Connections are tried one after another until one is successful: SocketConnectHost:SocketConnectPort, + SocketConnectHost1:SocketConnectPort1, etc.
    • +
    • Next connection attempt after a successful connection will start at first defined connection again: + SocketConnectHost:SocketConnectPort.
    • +
    + valid IP address in the format of x.x.x.x or a domain name   @@ -626,6 +665,58 @@

    QuickFIX Settings

    Java default cipher suites + + Socks Proxy Options (Initiator only) + + + ProxyType + Proxy type + http
    socks + + + + ProxyVersion + Proxy HTTP or Socks version to use + For socks: 4, 4a or 5
    For http: 1.0 or 1.1 + For socks:
    For http: 1.0 + + + ProxyHost + Proxy server hostname or IP + valid IP address in the format of x.x.x.x or a domain name + + + + ProxyPort + Proxy server port + positive integer + + + + ProxyUser + Proxy user + + + + + ProxyPassword + Proxy password + + + + + ProxyDomain + Proxy domain (For http proxy) + + + + + ProxyWorkstation + Proxy workstation (For http proxy) + + + + Socket Options (Acceptor or Initiator) diff --git a/quickfixj-core/src/main/java/org/quickfixj/SimpleCache.java b/quickfixj-core/src/main/java/org/quickfixj/SimpleCache.java new file mode 100644 index 000000000..17585d6ff --- /dev/null +++ b/quickfixj-core/src/main/java/org/quickfixj/SimpleCache.java @@ -0,0 +1,43 @@ +/* + ****************************************************************************** + * 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 org.quickfixj; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +public class SimpleCache extends ConcurrentHashMap { + static final int CONCURRENCY_LEVEL = Runtime.getRuntime().availableProcessors() * 2; + + final Function loadingFunction; + + public SimpleCache(Function loadingFunction) { + super(CONCURRENCY_LEVEL, 0.7f, CONCURRENCY_LEVEL); + this.loadingFunction = loadingFunction; + } + + public V computeIfAbsent(K key) { + /* + * We could computeIfAbsent directly but for CPUs < 32 pre-scanning is faster. + */ + final V value = get(key); + return value != null ? value : computeIfAbsent(key, loadingFunction); + } +} diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/JmxExporter.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/JmxExporter.java index c6da7e6c1..ac141e10f 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/JmxExporter.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/JmxExporter.java @@ -104,16 +104,6 @@ private static MBeanServer createMBeanServer() throws JMException { } } - /** - * Register a connector with JMX - * - * @deprecated use register instead - * @param connector - */ - public void export(Connector connector) { - register(connector); - } - public ObjectName register(Connector connector) { return connectorExporter.register(this, (SessionConnector) connector); } @@ -141,14 +131,10 @@ public void registerMBean(Object mbean, ObjectName objectName) throws JMExceptio mbeanServer.registerMBean(mbean, objectName); } catch (final InstanceAlreadyExistsException ex) { if (registrationBehaviour == REGISTRATION_IGNORE_EXISTING) { - if (log.isDebugEnabled()) { - log.debug("Ignoring existing MBean at [" + objectName + "]"); - } + log.debug("Ignoring existing MBean at [{}]", objectName); } else if (registrationBehaviour == REGISTRATION_REPLACE_EXISTING) { try { - if (log.isDebugEnabled()) { - log.debug("Replacing existing MBean at [" + objectName + "]"); - } + log.debug("Replacing existing MBean at [{}]", objectName); mbeanServer.unregisterMBean(objectName); mbeanServer.registerMBean(mbean, objectName); } catch (final InstanceNotFoundException ex2) { diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/ConnectorAdmin.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/ConnectorAdmin.java index 110218662..c4d48fa24 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/ConnectorAdmin.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/ConnectorAdmin.java @@ -17,20 +17,6 @@ package org.quickfixj.jmx.mbean.connector; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; - -import javax.management.MBeanRegistration; -import javax.management.MBeanServer; -import javax.management.ObjectName; -import javax.management.openmbean.OpenDataException; -import javax.management.openmbean.TabularData; - import org.quickfixj.QFJException; import org.quickfixj.jmx.JmxExporter; import org.quickfixj.jmx.mbean.JmxSupport; @@ -38,7 +24,6 @@ import org.quickfixj.jmx.openmbean.TabularDataAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import quickfix.Acceptor; import quickfix.Connector; import quickfix.Initiator; @@ -47,6 +32,17 @@ import quickfix.SessionSettings; import quickfix.mina.SessionConnector; +import javax.management.MBeanRegistration; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.TabularData; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + abstract class ConnectorAdmin implements ConnectorAdminMBean, MBeanRegistration { private final Logger log = LoggerFactory.getLogger(getClass()); @@ -64,7 +60,7 @@ abstract class ConnectorAdmin implements ConnectorAdminMBean, MBeanRegistration private final ObjectName connectorName; - private final List sessionNames = new ArrayList(); + private final List sessionNames = new ArrayList<>(); private final SessionSettings settings; @@ -118,7 +114,7 @@ public String getRemoteAddress() { } public TabularData getSessions() throws IOException { - List sessions = new ArrayList(); + List sessions = new ArrayList<>(); for (SessionID sessionID : connector.getSessions()) { Session session = Session.lookupSession(sessionID); sessions.add(new ConnectorSession(session, sessionExporter.getSessionName(sessionID))); @@ -131,7 +127,7 @@ public TabularData getSessions() throws IOException { } public TabularData getLoggedOnSessions() throws OpenDataException { - List names = new ArrayList(); + List names = new ArrayList<>(); for (SessionID sessionID : connector.getSessions()) { Session session = Session.lookupSession(sessionID); if (session.isLoggedOn()) { @@ -146,7 +142,7 @@ private ObjectName[] toObjectNameArray(List sessions) { } public void stop(boolean force) { - log.info("JMX operation: stop " + getRole() + " " + this); + log.info("JMX operation: stop {} {}", getRole(), this); connector.stop(force); } @@ -169,11 +165,9 @@ public ObjectName preRegister(MBeanServer server, ObjectName name) throws Except public void postRegister(Boolean registrationDone) { if (connector instanceof SessionConnector) { - ((SessionConnector) connector).addPropertyChangeListener(new PropertyChangeListener() { - public void propertyChange(PropertyChangeEvent evt) { - if (SessionConnector.SESSIONS_PROPERTY.equals(evt.getPropertyName())) { - registerSessions(); - } + ((SessionConnector) connector).addPropertyChangeListener(evt -> { + if (SessionConnector.SESSIONS_PROPERTY.equals(evt.getPropertyName())) { + registerSessions(); } }); } diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketAcceptorAdmin.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketAcceptorAdmin.java index 41d9b2411..19a066c6b 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketAcceptorAdmin.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketAcceptorAdmin.java @@ -17,25 +17,23 @@ package org.quickfixj.jmx.mbean.connector; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import javax.management.ObjectName; -import javax.management.openmbean.OpenDataException; -import javax.management.openmbean.TabularData; - import org.quickfixj.jmx.JmxExporter; import org.quickfixj.jmx.mbean.JmxSupport; import org.quickfixj.jmx.mbean.session.SessionJmxExporter; import org.quickfixj.jmx.openmbean.TabularDataAdapter; - import quickfix.SessionID; import quickfix.mina.acceptor.AbstractSocketAcceptor; +import javax.management.ObjectName; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.TabularData; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + /** * Implementation of the socket acceptor management interface. */ @@ -85,7 +83,7 @@ public ObjectName getSessionName() { } public TabularData getAcceptorAddresses() throws IOException { - List rows = new ArrayList(); + List rows = new ArrayList<>(); for (Map.Entry entry : acceptor.getAcceptorAddresses().entrySet()) { SessionID sessionID = entry.getKey(); SocketAddress address = entry.getValue(); diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketInitiatorAdmin.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketInitiatorAdmin.java index cf63af778..590e6dfa9 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketInitiatorAdmin.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketInitiatorAdmin.java @@ -17,20 +17,17 @@ package org.quickfixj.jmx.mbean.connector; -import java.io.IOException; -import java.util.ArrayList; - -import javax.management.ObjectName; -import javax.management.openmbean.OpenDataException; -import javax.management.openmbean.TabularData; - import org.quickfixj.jmx.JmxExporter; import org.quickfixj.jmx.mbean.JmxSupport; import org.quickfixj.jmx.mbean.session.SessionJmxExporter; import org.quickfixj.jmx.openmbean.TabularDataAdapter; - import quickfix.mina.initiator.AbstractSocketInitiator; -import quickfix.mina.initiator.IoSessionInitiator; + +import javax.management.ObjectName; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.TabularData; +import java.io.IOException; +import java.util.ArrayList; class SocketInitiatorAdmin extends ConnectorAdmin implements SocketInitiatorAdminMBean { @@ -47,7 +44,7 @@ protected SocketInitiatorAdmin(JmxExporter jmxExporter, AbstractSocketInitiator public TabularData getEndpoints() throws IOException { try { return tabularDataAdapter.fromBeanList("Endpoints", "Endpoint", "sessionID", - new ArrayList(initiator.getInitiators())); + new ArrayList<>(initiator.getInitiators())); } catch (OpenDataException e) { throw JmxSupport.toIOException(e); } 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 3a1ac52b2..e6c3810a0 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 @@ -20,13 +20,20 @@ import org.quickfixj.QFJException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import quickfix.*; +import quickfix.Message; +import quickfix.Session; +import quickfix.SessionID; +import quickfix.SessionNotFound; +import quickfix.SessionStateListener; import quickfix.field.MsgType; import quickfix.field.NewSeqNo; import quickfix.field.converter.UtcTimestampConverter; -import javax.management.*; - +import javax.management.MBeanRegistration; +import javax.management.MBeanServer; +import javax.management.Notification; +import javax.management.NotificationBroadcasterSupport; +import javax.management.ObjectName; import java.io.IOException; import java.util.ArrayList; @@ -167,7 +174,7 @@ public int getNextTargetMsgSeqNum() throws IOException { * @see quickfix.jmx.SessionMBean#getMessages(int, int) */ public String[] getMessages(int startSequence, int endSequence) throws IOException { - ArrayList messages = new ArrayList(); + ArrayList messages = new ArrayList<>(); session.getStore().get(startSequence, endSequence, messages); return messages.toArray(new String[messages.size()]); } diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionJmxExporter.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionJmxExporter.java index e5e7acdd3..c341752a4 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionJmxExporter.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionJmxExporter.java @@ -17,26 +17,24 @@ package org.quickfixj.jmx.mbean.session; -import static quickfix.SessionID.NOT_SET; - -import java.util.HashMap; -import java.util.Map; -import java.util.TreeMap; - -import javax.management.JMException; -import javax.management.MalformedObjectNameException; -import javax.management.ObjectName; - import org.quickfixj.jmx.JmxExporter; import org.quickfixj.jmx.mbean.ObjectNameFactory; - import quickfix.ConfigError; import quickfix.Session; import quickfix.SessionID; import quickfix.SessionSettings; +import javax.management.JMException; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +import static quickfix.SessionID.NOT_SET; + public class SessionJmxExporter { - private final Map sessionObjectNames = new HashMap(); + private final Map sessionObjectNames = new HashMap<>(); public ObjectName register(JmxExporter jmxExporter, Session session, ObjectName connectorName, SessionSettings settings) throws JMException, ConfigError { @@ -60,7 +58,7 @@ public ObjectName getSessionName(SessionID sessionID) { } public ObjectName createSessionName(SessionID sessionID) throws MalformedObjectNameException { - TreeMap properties = new TreeMap(); + TreeMap properties = new TreeMap<>(); properties.put("type", "Session"); ObjectNameFactory nameFactory = new ObjectNameFactory(); nameFactory.addProperty("type", "Session"); diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionSettingsAdmin.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionSettingsAdmin.java index 4099f08af..ffc516d94 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionSettingsAdmin.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/session/SessionSettingsAdmin.java @@ -17,10 +17,9 @@ package org.quickfixj.jmx.mbean.session; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Properties; +import quickfix.ConfigError; +import quickfix.SessionID; +import quickfix.SessionSettings; import javax.management.Attribute; import javax.management.AttributeList; @@ -31,10 +30,10 @@ import javax.management.MBeanException; import javax.management.MBeanInfo; import javax.management.ReflectionException; - -import quickfix.ConfigError; -import quickfix.SessionID; -import quickfix.SessionSettings; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; /** * This is a read-only view of a sessions settings. @@ -68,7 +67,7 @@ public AttributeList getAttributes(String[] attributeNames) { } public MBeanInfo getMBeanInfo() { - List attributeInfos = new ArrayList(); + List attributeInfos = new ArrayList<>(); for (Map.Entry entry : settings.entrySet()) { String name = (String) entry.getKey(); attributeInfos.add(new MBeanAttributeInfo(name, "Setting for " + name, entry.getValue().getClass().getName(), true, false, diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/CompositeDataFactory.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/CompositeDataFactory.java index 0085ce7f1..e95c068b3 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/CompositeDataFactory.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/CompositeDataFactory.java @@ -17,17 +17,16 @@ package org.quickfixj.jmx.openmbean; -import java.util.ArrayList; - import javax.management.openmbean.CompositeData; import javax.management.openmbean.CompositeDataSupport; import javax.management.openmbean.CompositeType; import javax.management.openmbean.OpenDataException; +import java.util.ArrayList; public class CompositeDataFactory { private final CompositeType compositeType; - private final ArrayList itemNames = new ArrayList(); - private final ArrayList itemValues = new ArrayList(); + private final ArrayList itemNames = new ArrayList<>(); + private final ArrayList itemValues = new ArrayList<>(); public CompositeDataFactory(CompositeType compositeType) { this.compositeType = compositeType; diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/CompositeTypeFactory.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/CompositeTypeFactory.java index 5f1bb8ca8..f7ceff2de 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/CompositeTypeFactory.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/CompositeTypeFactory.java @@ -17,11 +17,10 @@ package org.quickfixj.jmx.openmbean; -import java.util.ArrayList; - import javax.management.openmbean.CompositeType; import javax.management.openmbean.OpenDataException; import javax.management.openmbean.OpenType; +import java.util.ArrayList; // NOTE: Do not parameterize OpenType for Java6 since it will // be incompatible with Java 5 @@ -29,11 +28,11 @@ public class CompositeTypeFactory { private final String name; private final String description; - private final ArrayList itemNames = new ArrayList(); - private final ArrayList itemDescriptions = new ArrayList(); + private final ArrayList itemNames = new ArrayList<>(); + private final ArrayList itemDescriptions = new ArrayList<>(); @SuppressWarnings("rawtypes") // Java 5/6 incompatibility - private final ArrayList itemTypes = new ArrayList(); + private final ArrayList itemTypes = new ArrayList<>(); public CompositeTypeFactory(String name, String description) { this.name = name; diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/TabularDataAdapter.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/TabularDataAdapter.java index 615822770..0e02d321f 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/TabularDataAdapter.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/openmbean/TabularDataAdapter.java @@ -17,19 +17,18 @@ package org.quickfixj.jmx.openmbean; -import java.beans.BeanInfo; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - import javax.management.openmbean.CompositeType; import javax.management.openmbean.OpenDataException; import javax.management.openmbean.SimpleType; import javax.management.openmbean.TabularData; import javax.management.openmbean.TabularDataSupport; import javax.management.openmbean.TabularType; +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; public class TabularDataAdapter { @@ -113,7 +112,7 @@ public TabularData fromBeanList(String tableTypeName, String rowTypeName, String TabularData table; try { CompositeTypeFactory rowTypeFactory = new CompositeTypeFactory(rowTypeName, rowTypeName); - List indexNames = new ArrayList(); + List indexNames = new ArrayList<>(); indexNames.add(keyProperty); rowTypeFactory.defineItem(formatHeader(keyProperty), SimpleType.STRING); for (Object bean : beans) { diff --git a/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java b/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java new file mode 100644 index 000000000..7bddc45ea --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java @@ -0,0 +1,74 @@ +package quickfix; + +public abstract class AbstractSessionConnectorBuilder { + private final Class derived; + Application application; + MessageStoreFactory messageStoreFactory; + SessionSettings settings; + LogFactory logFactory; + MessageFactory messageFactory; + + int queueCapacity = -1; + int queueLowerWatermark = -1; + int queueUpperWatermark = -1; + + AbstractSessionConnectorBuilder(Class derived) { + this.derived = derived; + } + + public Derived withApplication(Application val) throws ConfigError { + application = val; + return derived.cast(this); + } + + public Derived withMessageStoreFactory(MessageStoreFactory val) throws ConfigError { + messageStoreFactory = val; + return derived.cast(this); + } + + public Derived withSettings(SessionSettings val) { + settings = val; + return derived.cast(this); + } + + public Derived withLogFactory(LogFactory val) throws ConfigError { + logFactory = val; + return derived.cast(this); + } + + public Derived withMessageFactory(MessageFactory val) throws ConfigError { + messageFactory = val; + return derived.cast(this); + } + + public Derived withQueueCapacity(int val) throws ConfigError { + if (queueLowerWatermark >= 0) { + throw new ConfigError("queue capacity and watermarks may not be configured together"); + } else if (queueCapacity < 0) { + throw new ConfigError("negative queue capacity"); + } + queueCapacity = val; + return derived.cast(this); + } + + public Derived withQueueWatermarks(int lower, int upper) throws ConfigError { + if (queueCapacity >= 0) { + throw new ConfigError("queue capacity and watermarks may not be configured together"); + } else if (queueLowerWatermark < 0 || queueUpperWatermark <= queueLowerWatermark) { + throw new ConfigError("invalid queue watermarks, required: 0 <= lower watermark < upper watermark"); + } + queueLowerWatermark = lower; + queueUpperWatermark = upper; + return derived.cast(this); + } + + public final Product build() throws ConfigError { + if (logFactory == null) { + logFactory = new ScreenLogFactory(settings); + } + + return doBuild(); + } + + protected abstract Product doBuild() throws ConfigError; +} diff --git a/quickfixj-core/src/main/java/quickfix/Acceptor.java b/quickfixj-core/src/main/java/quickfix/Acceptor.java index 7e1e22f4a..9a35dd9f2 100644 --- a/quickfixj-core/src/main/java/quickfix/Acceptor.java +++ b/quickfixj-core/src/main/java/quickfix/Acceptor.java @@ -27,20 +27,20 @@ public interface Acceptor extends Connector { /** * Acceptor setting specifying the socket protocol used to accept connections. */ - public static final String SETTING_SOCKET_ACCEPT_PROTOCOL = "SocketAcceptProtocol"; + String SETTING_SOCKET_ACCEPT_PROTOCOL = "SocketAcceptProtocol"; /** * Acceptor setting specifying port for accepting FIX client connections. */ - public static final String SETTING_SOCKET_ACCEPT_PORT = "SocketAcceptPort"; + String SETTING_SOCKET_ACCEPT_PORT = "SocketAcceptPort"; /** * Acceptor setting specifying local IP interface address for accepting connections. */ - public static final String SETTING_SOCKET_ACCEPT_ADDRESS = "SocketAcceptAddress"; + String SETTING_SOCKET_ACCEPT_ADDRESS = "SocketAcceptAddress"; /** * Acceptor setting specifying local IP interface address for accepting connections. */ - public static final String SETTING_ACCEPTOR_TEMPLATE = "AcceptorTemplate"; + String SETTING_ACCEPTOR_TEMPLATE = "AcceptorTemplate"; } diff --git a/quickfixj-core/src/main/java/quickfix/CachedFileStore.java b/quickfixj-core/src/main/java/quickfix/CachedFileStore.java index faa01aa71..7a0579391 100644 --- a/quickfixj-core/src/main/java/quickfix/CachedFileStore.java +++ b/quickfixj-core/src/main/java/quickfix/CachedFileStore.java @@ -19,6 +19,11 @@ package quickfix; +import org.quickfixj.CharsetSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quickfix.field.converter.UtcTimestampConverter; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; @@ -39,12 +44,6 @@ import java.util.Set; import java.util.TreeMap; -import org.quickfixj.CharsetSupport; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import quickfix.field.converter.UtcTimestampConverter; - /** * File store implementation. THIS CLASS IS PUBLIC ONLY TO MAINTAIN COMPATIBILITY WITH THE QUICKFIX JNI. IT SHOULD ONLY * BE CREATED USING A FACTORY. @@ -134,16 +133,13 @@ private void initializeCache() throws IOException { private void initializeSessionCreateTime() throws IOException { final File sessionTimeFile = new File(sessionFileName); if (sessionTimeFile.exists() && sessionTimeFile.length() > 0) { - final DataInputStream sessionTimeInput = new DataInputStream(new BufferedInputStream( - new FileInputStream(sessionTimeFile))); - try { + try (DataInputStream sessionTimeInput = new DataInputStream(new BufferedInputStream( + new FileInputStream(sessionTimeFile)))) { final Calendar c = SystemTime.getUtcCalendar(UtcTimestampConverter .convert(sessionTimeInput.readUTF())); cache.setCreationTime(c); } catch (final Exception e) { throw new IOException(e.getMessage()); - } finally { - sessionTimeInput.close(); } } else { storeSessionTimeStamp(); @@ -151,14 +147,11 @@ private void initializeSessionCreateTime() throws IOException { } private void storeSessionTimeStamp() throws IOException { - final DataOutputStream sessionTimeOutput = new DataOutputStream(new BufferedOutputStream( - new FileOutputStream(sessionFileName, false))); - try { + try (DataOutputStream sessionTimeOutput = new DataOutputStream(new BufferedOutputStream( + new FileOutputStream(sessionFileName, false)))) { final Date date = SystemTime.getDate(); cache.setCreationTime(SystemTime.getUtcCalendar(date)); sessionTimeOutput.writeUTF(UtcTimestampConverter.convert(date, true)); - } finally { - sessionTimeOutput.close(); } } @@ -187,17 +180,14 @@ private void initializeSequenceNumbers() throws IOException { private void initializeMessageIndex() throws IOException { final File headerFile = new File(headerFileName); if (headerFile.exists()) { - final DataInputStream headerDataInputStream = new DataInputStream( - new BufferedInputStream(new FileInputStream(headerFile))); - try { + try (DataInputStream headerDataInputStream = new DataInputStream( + new BufferedInputStream(new FileInputStream(headerFile)))) { while (headerDataInputStream.available() > 0) { final int sequenceNumber = headerDataInputStream.readInt(); final long offset = headerDataInputStream.readLong(); final int size = headerDataInputStream.readInt(); - messageIndex.put((long) sequenceNumber, new long[] { offset, size }); + messageIndex.put((long) sequenceNumber, new long[]{offset, size}); } - } finally { - headerDataInputStream.close(); } } headerFileOutputStream = new FileOutputStream(headerFileName, true); @@ -244,7 +234,7 @@ public void deleteFiles() throws IOException { private void deleteFile(String fileName) throws IOException { final File file = new File(fileName); if (file.exists() && !file.delete()) { - log.error("File delete failed: " + fileName); + log.error("File delete failed: {}", fileName); } } @@ -331,7 +321,7 @@ private String read(long offset, long size) throws IOException { } private Collection getMessage(long startSequence, long endSequence) throws IOException { - final Collection messages = new ArrayList(); + final Collection messages = new ArrayList<>(); final List offsetAndSizes = messageIndex.get(startSequence, endSequence); for (final long[] offsetAndSize : offsetAndSizes) { @@ -407,7 +397,7 @@ public void reset() throws IOException { */ private class CachedHashMap implements Map { - private final TreeMap cacheIndex = new TreeMap(); + private final TreeMap cacheIndex = new TreeMap<>(); private int currentSize; @@ -510,7 +500,7 @@ private long[] seekMessageIndex(final long index) { } private List seekMessageIndex(final long startSequence, final long endSequence) { - final TreeMap indexPerSequenceNumber = new TreeMap(); + final TreeMap indexPerSequenceNumber = new TreeMap<>(); final File headerFile = new File(headerFileName); if (headerFile.exists()) { DataInputStream headerDataInputStream = null; @@ -538,7 +528,7 @@ private List seekMessageIndex(final long startSequence, final long endSe } } } - return new ArrayList(indexPerSequenceNumber.values()); + return new ArrayList<>(indexPerSequenceNumber.values()); } public List get(final long startSequence, final long endSequence) { diff --git a/quickfixj-core/src/main/java/quickfix/CompositeLogFactory.java b/quickfixj-core/src/main/java/quickfix/CompositeLogFactory.java index 6a4fc12d0..660212452 100644 --- a/quickfixj-core/src/main/java/quickfix/CompositeLogFactory.java +++ b/quickfixj-core/src/main/java/quickfix/CompositeLogFactory.java @@ -58,8 +58,4 @@ public Log create(SessionID sessionID) { return new CompositeLog(logs); } - public Log create() { - throw new UnsupportedOperationException(); - } - } diff --git a/quickfixj-core/src/main/java/quickfix/Connector.java b/quickfixj-core/src/main/java/quickfix/Connector.java index 852973591..186425153 100644 --- a/quickfixj-core/src/main/java/quickfix/Connector.java +++ b/quickfixj-core/src/main/java/quickfix/Connector.java @@ -29,7 +29,8 @@ public interface Connector { /** * Start accepting connections. Returns immediately. See implementations of * this interface potential threading issues. - * + * This method must not be called by several threads concurrently. + * * @throws ConfigError Problem with acceptor configuration. * @throws RuntimeError Other unspecified error */ @@ -38,24 +39,17 @@ public interface Connector { /** * Logout existing sessions, close their connections, and stop accepting new * connections. + * This method must not be called by several threads concurrently. */ void stop(); /** * Stops all sessions, optionally waiting for logout completion. + * This method must not be called by several threads concurrently. * * @param force don't wait for logout before disconnect. */ - public void stop(boolean force); - - /** - * Start accepting connections. This method blocks until stop is called from - * another thread. - * - * @throws ConfigError Problem with acceptor configuration. - * @throws RuntimeError Other unspecified error - */ - void block() throws ConfigError, RuntimeError; + void stop(boolean force); /** * Checks the logged on status of the session. diff --git a/quickfixj-core/src/main/java/quickfix/DataDictionary.java b/quickfixj-core/src/main/java/quickfix/DataDictionary.java index bb36d9e54..99e991ab8 100644 --- a/quickfixj-core/src/main/java/quickfix/DataDictionary.java +++ b/quickfixj-core/src/main/java/quickfix/DataDictionary.java @@ -19,31 +19,11 @@ package quickfix; -import static quickfix.FileUtil.Location.CLASSLOADER_RESOURCE; -import static quickfix.FileUtil.Location.CONTEXT_RESOURCE; -import static quickfix.FileUtil.Location.FILESYSTEM; -import static quickfix.FileUtil.Location.URL; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; - import quickfix.field.BeginString; import quickfix.field.MsgType; import quickfix.field.SessionRejectReason; @@ -55,6 +35,24 @@ import quickfix.field.converter.UtcTimeOnlyConverter; import quickfix.field.converter.UtcTimestampConverter; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static quickfix.FileUtil.Location.CLASSLOADER_RESOURCE; +import static quickfix.FileUtil.Location.CONTEXT_RESOURCE; +import static quickfix.FileUtil.Location.FILESYSTEM; +import static quickfix.FileUtil.Location.URL; + /** * Provide the message metadata for various versions of FIX. */ @@ -76,19 +74,19 @@ public class DataDictionary { private boolean checkUnorderedGroupFields = true; private boolean allowUnknownMessageFields = false; private String beginString; - private final Map> messageFields = new HashMap>(); - private final Map> requiredFields = new HashMap>(); - private final Set messages = new HashSet(); - private final Map messageCategory = new HashMap(); - private final Map messageTypeForName = new HashMap(); - private final LinkedHashSet fields = new LinkedHashSet(); - private final Map fieldTypes = new HashMap(); - private final Map> fieldValues = new HashMap>(); - private final Map fieldNames = new HashMap(); - private final Map names = new HashMap(); - private final Map valueNames = new HashMap(); - private final Map groups = new HashMap(); - private final Map components = new HashMap(); + private final Map> messageFields = new HashMap<>(); + private final Map> requiredFields = new HashMap<>(); + private final Set messages = new HashSet<>(); + private final Map messageCategory = new HashMap<>(); + private final Map messageTypeForName = new HashMap<>(); + private final LinkedHashSet fields = new LinkedHashSet<>(); + private final Map fieldTypes = new HashMap<>(); + private final Map> fieldValues = new HashMap<>(); + private final Map fieldNames = new HashMap<>(); + private final Map names = new HashMap<>(); + private final Map valueNames = new HashMap<>(); + private final Map groups = new HashMap<>(); + private final Map components = new HashMap<>(); private DataDictionary() { } @@ -245,12 +243,7 @@ public boolean isAppMessage(String msgType) { } private void addMsgField(String msgType, int field) { - Set fields = messageFields.get(msgType); - if (fields == null) { - fields = new HashSet(); - messageFields.put(msgType, fields); - } - fields.add(field); + messageFields.computeIfAbsent(msgType, k -> new HashSet<>()).add(field); } /** @@ -301,12 +294,7 @@ public int getFieldTag(String name) { } private void addRequiredField(String msgType, int field) { - Set fields = requiredFields.get(msgType); - if (fields == null) { - fields = new HashSet(); - requiredFields.put(msgType, fields); - } - fields.add(field); + requiredFields.computeIfAbsent(msgType, k -> new HashSet<>()).add(field); } /** @@ -342,12 +330,7 @@ public boolean isRequiredTrailerField(int field) { } private void addFieldValue(int field, String value) { - Set values = fieldValues.get(field); - if (values == null) { - values = new HashSet(); - fieldValues.put(field, values); - } - values.add(value); + fieldValues.computeIfAbsent(field, k -> new HashSet<>()).add(value); } /** @@ -520,11 +503,6 @@ public void setAllowUnknownMessageFields(boolean allowUnknownFields) { private void copyFrom(DataDictionary rhs) { hasVersion = rhs.hasVersion; beginString = rhs.beginString; - checkFieldsOutOfOrder = rhs.checkFieldsOutOfOrder; - checkFieldsHaveValues = rhs.checkFieldsHaveValues; - checkUserDefinedFields = rhs.checkUserDefinedFields; - checkUnorderedGroupFields = rhs.checkUnorderedGroupFields; - allowUnknownMessageFields = rhs.allowUnknownMessageFields; copyMap(messageFields, rhs.messageFields); copyMap(requiredFields, rhs.requiredFields); @@ -535,8 +513,14 @@ private void copyFrom(DataDictionary rhs) { copyMap(fieldNames, rhs.fieldNames); copyMap(names, rhs.names); copyMap(valueNames, rhs.valueNames); - copyMap(groups, rhs.groups); + copyGroups(groups, rhs.groups); copyMap(components, rhs.components); + + setCheckFieldsOutOfOrder(rhs.checkFieldsOutOfOrder); + setCheckFieldsHaveValues(rhs.checkFieldsHaveValues); + setCheckUserDefinedFields(rhs.checkUserDefinedFields); + setCheckUnorderedGroupFields(rhs.checkUnorderedGroupFields); + setAllowUnknownMessageFields(rhs.allowUnknownMessageFields); } @SuppressWarnings("unchecked") @@ -560,18 +544,31 @@ private static void copyMap(Map lhs, Map rhs) { } } + /** copy groups including their data dictionaries and validation settings + * + * @param lhs target + * @param rhs source + */ + private static void copyGroups(Map lhs, Map rhs) { + lhs.clear(); + for (Map.Entry entry : rhs.entrySet()) { + GroupInfo value = new GroupInfo(entry.getValue().getDelimiterField(), new DataDictionary(entry.getValue().getDataDictionary())); + lhs.put(entry.getKey(), value); + } + } + private static void copyCollection(Collection lhs, Collection rhs) { lhs.clear(); lhs.addAll(rhs); } /** - * Validate a mesasge, including the header and trailer fields. + * Validate a message, 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 + * @throws IncorrectDataFormat if a field value has a wrong data type */ public void validate(Message message) throws IncorrectTagValue, FieldNotFound, IncorrectDataFormat { @@ -585,7 +582,7 @@ public void validate(Message message) throws IncorrectTagValue, FieldNotFound, * @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 + * @throws IncorrectDataFormat if a field value has a wrong data type */ public void validate(Message message, boolean bodyOnly) throws IncorrectTagValue, FieldNotFound, IncorrectDataFormat { @@ -656,36 +653,28 @@ private void iterate(FieldMap map, String msgType, DataDictionary dd) throws Inc } } - // / Check if message type is defined in spec. + /** Check if message type is defined in spec. **/ private void checkMsgType(String msgType) { if (!isMsgType(msgType)) { - // It would be better to include the msgType in exception message - // Doing that will break acceptance tests - throw new FieldException(SessionRejectReason.INVALID_MSGTYPE); + throw new FieldException(SessionRejectReason.INVALID_MSGTYPE, MsgType.FIELD); } } - // / Check if field tag number is defined in spec. + /** Check if field tag number is defined in spec. **/ 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 + /** Check if field tag is defined for message or group **/ void checkField(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; - - if (field.getField() < USER_DEFINED_TAG_MIN) { - fail = !messageField && !allowUnknownMessageFields; - } else { - fail = !messageField && checkUserDefinedFields; - } + boolean fail = checkFieldFailure(field.getField(), messageField); if (fail) { - if (fields.contains(field.getTag())) { + if (fields.contains(field.getField())) { throw new FieldException(SessionRejectReason.TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE, field.getField()); } else { throw new FieldException(SessionRejectReason.INVALID_TAG_NUMBER, field.getField()); @@ -693,6 +682,16 @@ void checkField(Field field, String msgType, boolean message) { } } + boolean checkFieldFailure(int field, boolean messageField) { + boolean fail; + if (field < USER_DEFINED_TAG_MIN) { + fail = !messageField && !allowUnknownMessageFields; + } else { + fail = !messageField && checkUserDefinedFields; + } + return fail; + } + private void checkValidFormat(StringField field) throws IncorrectDataFormat { FieldType fieldType = getFieldType(field.getTag()); if (fieldType == null) { @@ -756,7 +755,7 @@ private void checkValue(StringField field) throws IncorrectTagValue { } } - // / Check if a field has a value. + /** Check if a field has a value. **/ private void checkHasValue(StringField field) { if (checkFieldsHaveValues && field.getValue().length() == 0) { throw new FieldException(SessionRejectReason.TAG_SPECIFIED_WITHOUT_A_VALUE, @@ -764,7 +763,7 @@ private void checkHasValue(StringField field) { } } - // / Check if group count matches number of groups in + /** Check if group count matches number of groups in **/ private void checkGroupCount(StringField field, FieldMap fieldMap, String msgType) { final int fieldNum = field.getField(); if (isGroup(msgType, fieldNum)) { @@ -776,7 +775,7 @@ private void checkGroupCount(StringField field, FieldMap fieldMap, String msgTyp } } - // / Check if a message has all required fields. + /** Check if a message has all required fields. **/ void checkHasRequired(FieldMap header, FieldMap body, FieldMap trailer, String msgType, boolean bodyOnly) { if (!bodyOnly) { @@ -813,6 +812,18 @@ private void checkHasRequired(String msgType, FieldMap fields, boolean bodyOnly) } } + private int countElementNodes(NodeList nodes) { + int elementNodesCount = 0; + + for (int i = 0; i < nodes.getLength(); i++) { + if (nodes.item(i).getNodeType() == Node.ELEMENT_NODE) { + elementNodesCount++; + } + } + + return elementNodesCount; + } + private void read(String location) throws ConfigError { final InputStream inputStream = FileUtil.open(getClass(), location, URL, FILESYSTEM, CONTEXT_RESOURCE, CLASSLOADER_RESOURCE); @@ -886,7 +897,7 @@ private void load(InputStream inputStream) throws ConfigError { } final NodeList fieldNodes = fieldsNode.item(0).getChildNodes(); - if (fieldNodes.getLength() == 0) { + if (countElementNodes(fieldNodes) == 0) { throw new ConfigError("No fields defined"); } @@ -965,7 +976,7 @@ private void load(InputStream inputStream) throws ConfigError { } final NodeList messageNodes = messagesNode.item(0).getChildNodes(); - if (messageNodes.getLength() == 0) { + if (countElementNodes(messageNodes) == 0) { throw new ConfigError("No messages defined"); } @@ -1001,7 +1012,7 @@ public int getNumMessageCategories() { private void load(Document document, String msgtype, Node node) throws ConfigError { String name; final NodeList fieldNodes = node.getChildNodes(); - if (fieldNodes.getLength() == 0) { + if (countElementNodes(fieldNodes) == 0) { throw new ConfigError("No fields found: msgType=" + msgtype); } @@ -1106,7 +1117,7 @@ private int addXMLComponentFields(Document document, Node node, String msgtype, final String required = getAttribute(componentFieldNode, "required"); if (required.equalsIgnoreCase("Y") && componentRequired) { - addRequiredField(msgtype, field); + dd.addRequiredField(msgtype, field); } dd.addField(field); @@ -1149,7 +1160,9 @@ private void addXMLGroup(Document document, Node node, String msgtype, DataDicti groupDD.addRequiredField(msgtype, field); } } else if (fieldNode.getNodeName().equals("component")) { - field = addXMLComponentFields(document, fieldNode, msgtype, groupDD, false); + final String required = getAttribute(fieldNode, "required"); + final boolean isRequired = required != null && required.equalsIgnoreCase("Y"); + field = addXMLComponentFields(document, fieldNode, msgtype, groupDD, isRequired); } else if (fieldNode.getNodeName().equals("group")) { field = lookupXMLFieldNumber(document, fieldNode); groupDD.addField(field); @@ -1232,17 +1245,6 @@ public DataDictionary getDataDictionary() { return dataDictionary; } - /** - * Returns the delimiter field used to start a repeating group instance. - * - * @return delimiter field - * @deprecated use getDelimiterField() instead - */ - @Deprecated - public int getDelimeterField() { - return delimiterField; - } - /** * Returns the delimiter field used to start a repeating group instance. * diff --git a/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProvider.java b/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProvider.java index 3af287101..63eb353b8 100644 --- a/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProvider.java +++ b/quickfixj-core/src/main/java/quickfix/DefaultDataDictionaryProvider.java @@ -19,56 +19,52 @@ package quickfix; -import static quickfix.MessageUtils.*; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - import org.quickfixj.QFJException; - +import org.quickfixj.SimpleCache; import quickfix.field.ApplVerID; +import static quickfix.MessageUtils.toBeginString; + public class DefaultDataDictionaryProvider implements DataDictionaryProvider { - private final Map transportDictionaries = new ConcurrentHashMap(); - private final Map applicationDictionaries = new ConcurrentHashMap(); - private final boolean findDataDictionaries; + private final SimpleCache transportDictionaries; + private final SimpleCache applicationDictionaries; public DefaultDataDictionaryProvider() { - findDataDictionaries = true; + this(true); } public DefaultDataDictionaryProvider(boolean findDataDictionaries) { - this.findDataDictionaries = findDataDictionaries; + transportDictionaries = new SimpleCache<>(beginString -> { + if (findDataDictionaries) { + final String path = beginString.replace(".", "") + ".xml"; + try { + return new DataDictionary(path); + } catch (ConfigError e) { + throw new QFJException(e); + } + } + return null; + }); + applicationDictionaries = new SimpleCache<>(applVerID -> { + if (findDataDictionaries) { + final String beginString = toBeginString(applVerID); + final String path = beginString.replace(".", "") + ".xml"; + try { + return new DataDictionary(path); + } catch (ConfigError e) { + throw new QFJException(e); + } + } + return null; + }); } - public synchronized DataDictionary getSessionDataDictionary(String beginString) { - DataDictionary dd = transportDictionaries.get(beginString); - if (dd == null && findDataDictionaries) { - String path = beginString.replace(".", "") + ".xml"; - try { - dd = new DataDictionary(path); - transportDictionaries.put(beginString, dd); - } catch (ConfigError e) { - throw new QFJException(e); - } - } - return dd; + public DataDictionary getSessionDataDictionary(String beginString) { + return transportDictionaries.computeIfAbsent(beginString); } public DataDictionary getApplicationDataDictionary(ApplVerID applVerID) { - AppVersionKey appVersionKey = new AppVersionKey(applVerID); - DataDictionary dd = applicationDictionaries.get(appVersionKey); - if (dd == null && findDataDictionaries) { - String beginString = toBeginString(applVerID); - String path = beginString.replace(".", "") + ".xml"; - try { - dd = new DataDictionary(path); - applicationDictionaries.put(appVersionKey, dd); - } catch (ConfigError e) { - throw new QFJException(e); - } - } - return dd; + return applicationDictionaries.computeIfAbsent(applVerID); } public void addTransportDictionary(String beginString, DataDictionary dd) { @@ -76,44 +72,6 @@ public void addTransportDictionary(String beginString, DataDictionary dd) { } public void addApplicationDictionary(ApplVerID applVerID, DataDictionary dataDictionary) { - applicationDictionaries.put(new AppVersionKey(applVerID), dataDictionary); - } - - private static class AppVersionKey { - private final ApplVerID applVerID; - - public AppVersionKey(ApplVerID applVerID) { - this.applVerID = applVerID; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((applVerID == null) ? 0 : applVerID.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - AppVersionKey other = (AppVersionKey) obj; - if (applVerID == null) { - if (other.applVerID != null) { - return false; - } - } else if (!applVerID.equals(other.applVerID)) { - return false; - } - return true; - } + applicationDictionaries.put(applVerID, dataDictionary); } } diff --git a/quickfixj-core/src/main/java/quickfix/DefaultMessageFactory.java b/quickfixj-core/src/main/java/quickfix/DefaultMessageFactory.java index f942e0c50..744249abe 100644 --- a/quickfixj-core/src/main/java/quickfix/DefaultMessageFactory.java +++ b/quickfixj-core/src/main/java/quickfix/DefaultMessageFactory.java @@ -19,18 +19,30 @@ package quickfix; -import static quickfix.FixVersions.*; +import quickfix.field.ApplVerID; import quickfix.field.MsgType; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import static quickfix.FixVersions.BEGINSTRING_FIX40; +import static quickfix.FixVersions.BEGINSTRING_FIX41; +import static quickfix.FixVersions.BEGINSTRING_FIX42; +import static quickfix.FixVersions.BEGINSTRING_FIX43; +import static quickfix.FixVersions.BEGINSTRING_FIX44; +import static quickfix.FixVersions.BEGINSTRING_FIXT11; +import static quickfix.FixVersions.FIX50; +import static quickfix.FixVersions.FIX50SP1; +import static quickfix.FixVersions.FIX50SP2; + /** * The default factory for creating FIX message instances. */ public class DefaultMessageFactory implements MessageFactory { - private final Map messageFactories - = new ConcurrentHashMap(); + private final Map messageFactories = new ConcurrentHashMap<>(); + + private final ApplVerID defaultApplVerID; /** * Constructs a DefaultMessageFactory, which dynamically loads and delegates to @@ -39,8 +51,28 @@ public class DefaultMessageFactory implements MessageFactory { * Callers can set the {@link Thread#setContextClassLoader context classloader}, * which will be used to load the classes if {@link Class#forName Class.forName} * fails to do so (e.g. in an OSGi environment). + *

    + * Equivalent to {@link #DefaultMessageFactory(String) DefaultMessageFactory}({@link ApplVerID#FIX50 ApplVerID.FIX50}). */ public DefaultMessageFactory() { + this(ApplVerID.FIX50); + } + + /** + * Constructs a DefaultMessageFactory, which dynamically loads and delegates to + * the default version-specific message factories, if they are available at runtime. + *

    + * Callers can set the {@link Thread#setContextClassLoader context classloader}, + * which will be used to load the classes if {@link Class#forName Class.forName} + * fails to do so (e.g. in an OSGi environment). + * + * @param defaultApplVerID ApplVerID value used by default for {@link #create(String, ApplVerID, String)} + */ + public DefaultMessageFactory(String defaultApplVerID) { + Objects.requireNonNull(defaultApplVerID, "defaultApplVerID"); + + this.defaultApplVerID = new ApplVerID(defaultApplVerID); + // To loosen the coupling between this factory and generated code, the // message factories are discovered at run time using reflection addFactory(BEGINSTRING_FIX40); @@ -110,27 +142,23 @@ public void addFactory(String beginString, Class facto } } + @Override public Message create(String beginString, String msgType) { + return create(beginString, defaultApplVerID, msgType); + } + + @Override + public Message create(String beginString, ApplVerID applVerID, String msgType) { MessageFactory messageFactory = messageFactories.get(beginString); - if (beginString.equals(BEGINSTRING_FIXT11)) { - // The default message factory assumes that only FIX 5.0 will be - // used with FIXT 1.1 sessions. A more flexible approach will require - // an extension to the QF JNI API. Until then, you will need a custom - // message factory if you want to use application messages prior to - // FIX 5.0 with a FIXT 1.1 session. - // - // TODO: how do we support 50/50SP1/50SP2 concurrently? - // - // If you need to determine admin message category based on a data - // dictionary, then use a custom message factory and don't use the - // static method used below. - if (!MessageUtils.isAdminMessage(msgType)) { - messageFactory = messageFactories.get(FIX50); + if (beginString.equals(BEGINSTRING_FIXT11) && !MessageUtils.isAdminMessage(msgType)) { + if (applVerID == null) { + applVerID = new ApplVerID(defaultApplVerID.getValue()); } + messageFactory = messageFactories.get(MessageUtils.toBeginString(applVerID)); } if (messageFactory != null) { - return messageFactory.create(beginString, msgType); + return messageFactory.create(beginString, applVerID, msgType); } Message message = new Message(); diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java index 50943cbb3..c7c7ba3a1 100644 --- a/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java +++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java @@ -19,26 +19,35 @@ package quickfix; +import org.quickfixj.QFJException; +import org.quickfixj.SimpleCache; +import quickfix.field.ApplVerID; +import quickfix.field.DefaultApplVerID; + import java.net.InetAddress; +import java.util.Arrays; import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Map; import java.util.Properties; import java.util.Set; -import quickfix.field.ApplVerID; -import quickfix.field.DefaultApplVerID; - /** * Factory for creating sessions. Used by the communications code (acceptors, * initiators) for creating sessions. */ public class DefaultSessionFactory implements SessionFactory { - private static final Map dictionaryCache = new Hashtable(); + private static final SimpleCache dictionaryCache = new SimpleCache<>(path -> { + try { + return new DataDictionary(path); + } catch (ConfigError e) { + throw new QFJException(e); + } + }); + private final Application application; private final MessageStoreFactory messageStoreFactory; private final LogFactory logFactory; private final MessageFactory messageFactory; + private final SessionScheduleFactory sessionScheduleFactory; public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory, LogFactory logFactory) { @@ -46,6 +55,7 @@ public DefaultSessionFactory(Application application, MessageStoreFactory messag this.messageStoreFactory = messageStoreFactory; this.logFactory = logFactory; this.messageFactory = new DefaultMessageFactory(); + this.sessionScheduleFactory = new DefaultSessionScheduleFactory(); } public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory, @@ -54,6 +64,17 @@ public DefaultSessionFactory(Application application, MessageStoreFactory messag this.messageStoreFactory = messageStoreFactory; this.logFactory = logFactory; this.messageFactory = messageFactory; + this.sessionScheduleFactory = new DefaultSessionScheduleFactory(); + } + + public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory, + LogFactory logFactory, MessageFactory messageFactory, + SessionScheduleFactory sessionScheduleFactory) { + this.application = application; + this.messageStoreFactory = messageStoreFactory; + this.logFactory = logFactory; + this.messageFactory = messageFactory; + this.sessionScheduleFactory = sessionScheduleFactory; } public Session create(SessionID sessionID, SessionSettings settings) throws ConfigError { @@ -137,8 +158,8 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf Session.SETTING_TEST_REQUEST_DELAY_MULTIPLIER, Session.DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER); - final boolean millisInTimestamp = getSetting(settings, sessionID, - Session.SETTING_MILLISECONDS_IN_TIMESTAMP, true); + final UtcTimestampPrecision timestampPrecision = getTimestampPrecision(settings, sessionID, + UtcTimestampPrecision.MILLIS); final boolean resetOnLogout = getSetting(settings, sessionID, Session.SETTING_RESET_ON_LOGOUT, false); @@ -180,9 +201,11 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf final int[] logonIntervals = getLogonIntervalsInSeconds(settings, sessionID); final Set allowedRemoteAddresses = getInetAddresses(settings, sessionID); + final SessionSchedule sessionSchedule = sessionScheduleFactory.create(sessionID, settings); + final Session session = new Session(application, messageStoreFactory, sessionID, - dataDictionaryProvider, new SessionSchedule(settings, sessionID), logFactory, - messageFactory, heartbeatInterval, checkLatency, maxLatency, millisInTimestamp, + dataDictionaryProvider, sessionSchedule, logFactory, + messageFactory, heartbeatInterval, checkLatency, maxLatency, timestampPrecision, resetOnLogon, resetOnLogout, resetOnDisconnect, refreshAtLogon, checkCompID, redundantResentRequestAllowed, persistMessages, useClosedIntervalForResend, testRequestDelayMultiplier, senderDefaultApplVerID, validateSequenceNumbers, @@ -320,13 +343,14 @@ private String toDictionaryPath(String beginString) { } private DataDictionary getDataDictionary(String path) throws ConfigError { - synchronized (dictionaryCache) { - DataDictionary dataDictionary = dictionaryCache.get(path); - if (dataDictionary == null) { - dataDictionary = new DataDictionary(path); - dictionaryCache.put(path, dataDictionary); + try { + return dictionaryCache.computeIfAbsent(path); + } catch (QFJException e) { + final Throwable cause = e.getCause(); + if (cause instanceof ConfigError) { + throw (ConfigError) cause; } - return dataDictionary; + throw e; } } @@ -377,4 +401,18 @@ private double getSetting(SessionSettings settings, SessionID sessionID, String : defaultValue; } + private UtcTimestampPrecision getTimestampPrecision(SessionSettings settings, SessionID sessionID, + UtcTimestampPrecision defaultValue) throws ConfigError, FieldConvertError { + if (settings.isSetting(sessionID, Session.SETTING_TIMESTAMP_PRECISION)) { + String string = settings.getString(sessionID, Session.SETTING_TIMESTAMP_PRECISION); + try { + return UtcTimestampPrecision.valueOf(string); + } catch (IllegalArgumentException e) { + throw new ConfigError(e.getMessage() + ". Valid values: " + Arrays.toString(UtcTimestampPrecision.values())); + } + } else { + return defaultValue; + } + } + } diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java new file mode 100644 index 000000000..83d8bb5ea --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java @@ -0,0 +1,407 @@ +/******************************************************************************* + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Corresponds to SessionTime in C++ code + */ +public class DefaultSessionSchedule implements SessionSchedule { + 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; + protected static final Logger LOG = LoggerFactory.getLogger(DefaultSessionSchedule.class); + + public DefaultSessionSchedule(SessionSettings settings, SessionID sessionID) throws ConfigError, + FieldConvertError { + + 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; + } else { + isWeekdaySession = settings.isSetting(sessionID, Session.SETTING_WEEKDAYS); + } + + boolean startDayPresent = settings.isSetting(sessionID, Session.SETTING_START_DAY); + boolean endDayPresent = settings.isSetting(sessionID, Session.SETTING_END_DAY); + + if (isWeekdaySession) { + 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); + if (weekdayNames.isEmpty()) + throw new ConfigError("Session " + sessionID + ": " + Session.SETTING_WEEKDAYS + " is empty"); + + String[] weekdayNameArray = weekdayNames.split(","); + weekdayOffsets = new int[weekdayNameArray.length]; + for (int i = 0; i < weekdayNameArray.length; i++) { + weekdayOffsets[i] = DayConverter.toInteger(weekdayNameArray[i]); + } + } else { + weekdayOffsets = new int[0]; + + if (startDayPresent && !endDayPresent) { + throw new ConfigError("Session " + sessionID + ": StartDay used without EndDay"); + } + + if (endDayPresent && !startDayPresent) { + throw new ConfigError("Session " + sessionID + ": EndDay used without StartDay"); + } + } + 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()); + } + + private TimeEndPoint getTimeEndPoint(SessionSettings settings, SessionID sessionID, + TimeZone defaultTimeZone, String timeSetting, String daySetting) throws ConfigError, + FieldConvertError { + + Matcher matcher = TIME_PATTERN.matcher(settings.getString(sessionID, timeSetting)); + if (!matcher.find()) { + throw new ConfigError("Session " + sessionID + ": could not parse time '" + + settings.getString(sessionID, timeSetting) + "'."); + } + + return new TimeEndPoint( + getDay(settings, sessionID, daySetting, NOT_SET), + 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 { + TimeZone sessionTimeZone; + if (settings.isSetting(sessionID, Session.SETTING_TIMEZONE)) { + String sessionTimeZoneID = settings.getString(sessionID, Session.SETTING_TIMEZONE); + sessionTimeZone = TimeZone.getTimeZone(sessionTimeZoneID); + if ("GMT".equals(sessionTimeZone.getID()) && !"GMT".equals(sessionTimeZoneID)) { + throw new ConfigError("Unrecognized time zone '" + sessionTimeZoneID + + "' for session " + sessionID); + } + } else { + sessionTimeZone = TimeZone.getTimeZone("UTC"); + } + return sessionTimeZone; + } + + private TimeZone getTimeZone(String tz, TimeZone defaultZone) { + return "".equals(tz) ? defaultZone : TimeZone.getTimeZone(tz.trim()); + } + + private static class TimeEndPoint { + private final int weekDay; + private final int hour; + private final int minute; + private final int second; + private final int timeInSeconds; + private final TimeZone tz; + + public TimeEndPoint(int day, int hour, int minute, int second, TimeZone tz) { + weekDay = day; + this.hour = hour; + this.minute = minute; + this.second = second; + this.tz = tz; + timeInSeconds = timeInSeconds(hour, minute, second); + } + + int getHour() { + return hour; + } + + int getMinute() { + return minute; + } + + int getSecond() { + return second; + } + + int getDay() { + return weekDay; + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TimeEndPoint) { + TimeEndPoint otherTime = (TimeEndPoint) o; + return timeInSeconds == otherTime.timeInSeconds; + } + return false; + } + + public int hashCode() { + assert false : "hashCode not supported"; + return 0; + } + + TimeZone getTimeZone() { + return tz; + } + } + + /** + * find the most recent session date/time range on or before t + * if t is in a session then that session will be returned + * @param t specific date/time + * @return relevant session date/time range + */ + private TimeInterval theMostRecentIntervalBefore(Calendar t) { + TimeInterval timeInterval = new TimeInterval(); + Calendar intervalStart = timeInterval.getStart(); + intervalStart.setTimeZone(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.MILLISECOND, 0); + + Calendar intervalEnd = timeInterval.getEnd(); + intervalEnd.setTimeZone(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.MILLISECOND, 0); + + if (isWeekdaySession) { + while (intervalStart.getTimeInMillis() > t.getTimeInMillis() || + !validDayOfWeek(intervalStart)) { + intervalStart.add(Calendar.DAY_OF_WEEK, -1); + intervalEnd.add(Calendar.DAY_OF_WEEK, -1); + } + + if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) { + intervalEnd.add(Calendar.DAY_OF_WEEK, 1); + } + + } else { + if (isSet(startTime.getDay())) { + intervalStart.set(Calendar.DAY_OF_WEEK, startTime.getDay()); + if (intervalStart.getTimeInMillis() > t.getTimeInMillis()) { + intervalStart.add(Calendar.WEEK_OF_YEAR, -1); + intervalEnd.add(Calendar.WEEK_OF_YEAR, -1); + } + } else if (intervalStart.getTimeInMillis() > t.getTimeInMillis()) { + intervalStart.add(Calendar.DAY_OF_YEAR, -1); + intervalEnd.add(Calendar.DAY_OF_YEAR, -1); + } + + if (isSet(endTime.getDay())) { + intervalEnd.set(Calendar.DAY_OF_WEEK, endTime.getDay()); + if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) { + intervalEnd.add(Calendar.WEEK_OF_MONTH, 1); + } + } else if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) { + intervalEnd.add(Calendar.DAY_OF_WEEK, 1); + } + } + + return timeInterval; + } + + private static class TimeInterval { + private final Calendar start = SystemTime.getUtcCalendar(); + private final Calendar end = SystemTime.getUtcCalendar(); + + boolean isContainingTime(Calendar t) { + return t.compareTo(start) >= 0 && t.compareTo(end) <= 0; + } + + public String toString() { + return start.getTime() + " --> " + end.getTime(); + } + + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof TimeInterval)) { + return false; + } + TimeInterval otherInterval = (TimeInterval) other; + return start.equals(otherInterval.start) && end.equals(otherInterval.end); + } + + public int hashCode() { + assert false : "hashCode not supported"; + return 0; + } + + Calendar getStart() { + return start; + } + + Calendar getEnd() { + return end; + } + } + + @Override + public boolean isSameSession(Calendar time1, Calendar time2) { + if (isNonStopSession()) + return true; + TimeInterval interval1 = theMostRecentIntervalBefore(time1); + if (!interval1.isContainingTime(time1)) { + return false; + } + TimeInterval interval2 = theMostRecentIntervalBefore(time2); + return interval2.isContainingTime(time2) && interval1.equals(interval2); + } + + @Override + public boolean isNonStopSession() { + return isNonStopSession; + } + + private boolean isDailySession() { + return !isSet(startTime.getDay()) && !isSet(endTime.getDay()); + } + + @Override + public boolean isSessionTime() { + if(isNonStopSession()) { + return true; + } + Calendar now = SystemTime.getUtcCalendar(); + TimeInterval interval = theMostRecentIntervalBefore(now); + return interval.isContainingTime(now); + } + + public String toString() { + StringBuilder buf = new StringBuilder(); + + SimpleDateFormat dowFormat = new SimpleDateFormat("EEEE"); + dowFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + + SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss-z"); + timeFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + + TimeInterval ti = theMostRecentIntervalBefore(SystemTime.getUtcCalendar()); + + formatTimeInterval(buf, ti, timeFormat, false); + + // Now the localized equivalents, if necessary + if (!startTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE) + || !endTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE)) { + buf.append(" ("); + formatTimeInterval(buf, ti, timeFormat, 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()); + buf.append(" "); + } else { + buf.append("daily, "); + } + + if (local) { + timeFormat.setTimeZone(startTime.getTimeZone()); + } + buf.append(timeFormat.format(timeInterval.getStart().getTime())); + + buf.append(" - "); + + if (!isDailySession()) { + formatDayOfWeek(buf, endTime.getDay()); + buf.append(" "); + } + if (local) { + timeFormat.setTimeZone(endTime.getTimeZone()); + } + buf.append(timeFormat.format(timeInterval.getEnd().getTime())); + } + + private void formatDayOfWeek(StringBuilder buf, int dayOfWeek) { + try { + String dayName = DayConverter.toString(dayOfWeek).toUpperCase(); + if (dayName.length() > 3) { + dayName = dayName.substring(0, 3); + } + buf.append(dayName); + } catch (ConfigError e) { + buf.append("[Error: unknown day ").append(dayOfWeek).append("]"); + } + } + + private int getDay(SessionSettings settings, SessionID sessionID, String key, int defaultValue) + throws ConfigError, FieldConvertError { + return settings.isSetting(sessionID, key) ? + DayConverter.toInteger(settings.getString(sessionID, key)) + : NOT_SET; + } + + private boolean isSet(int value) { + return value != NOT_SET; + } + + private static int timeInSeconds(int hour, int minute, int second) { + return (hour * 3600) + (minute * 60) + second; + } + + /** + * is the startDateTime a valid day based on the permitted days of week + * @param startDateTime time to test + * @return flag indicating if valid + */ + private boolean validDayOfWeek(Calendar startDateTime) { + int dow = startDateTime.get(Calendar.DAY_OF_WEEK); + for (int i = 0; i < weekdayOffsets.length; i++) + if (weekdayOffsets[i] == dow) + return true; + return false; + } +} diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionScheduleFactory.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionScheduleFactory.java new file mode 100644 index 000000000..b25f4debc --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionScheduleFactory.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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; + +/** + * Factory for creating default session schedules. + */ +public class DefaultSessionScheduleFactory implements SessionScheduleFactory { + + public SessionSchedule create(SessionID sessionID, SessionSettings settings) throws ConfigError + { + try { + return new DefaultSessionSchedule(settings, sessionID); + } catch (final FieldConvertError e) { + throw new ConfigError(e.getMessage()); + } + } +} diff --git a/quickfixj-core/src/main/java/quickfix/Dictionary.java b/quickfixj-core/src/main/java/quickfix/Dictionary.java index 3d3310d9e..ab7989b42 100644 --- a/quickfixj-core/src/main/java/quickfix/Dictionary.java +++ b/quickfixj-core/src/main/java/quickfix/Dictionary.java @@ -29,13 +29,13 @@ */ public class Dictionary { private String name; - private final HashMap data = new HashMap(); + private final HashMap data = new HashMap<>(); public Dictionary() { } public Dictionary(String name) { - this(name, new HashMap()); + this(name, new HashMap<>()); } public Dictionary(Dictionary dictionary) { diff --git a/quickfixj-core/src/main/java/quickfix/ExecutorFactory.java b/quickfixj-core/src/main/java/quickfix/ExecutorFactory.java new file mode 100644 index 000000000..47f4e0c5b --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/ExecutorFactory.java @@ -0,0 +1,32 @@ +package quickfix; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + *

    + * Two Executors are required. The {@link #getLongLivedExecutor()} is used for message processing and the + * {@link #getShortLivedExecutor()} is used for timer tasks. They can both be the same underlying Executor if desired. + * Separating them allows for additional control such as with a ResourceAdapter WorkManager where the WorkManager + * differentiates between short and long lived Work. + *

    + *

    + * By way of example, a single {@link Executors#newCachedThreadPool()} satisfies the requirements but really adds + * nothing over the default behavior. + *

    + */ +public interface ExecutorFactory { + + /** + * The message processing activities are long-lived so this Executor must have sufficient distinct Threads available + * to handle all your Sessions. The exact number will depend on how many concurrent Sessions you will have and + * whether you are using the Threaded or non-Threaded SocketAcceptors/Initiators. + */ + Executor getLongLivedExecutor(); + + /** + * The timer tasks are short-lived and only require one Thread (calls are serialized). + */ + Executor getShortLivedExecutor(); + +} diff --git a/quickfixj-core/src/main/java/quickfix/FieldException.java b/quickfixj-core/src/main/java/quickfix/FieldException.java index 4e07e0f3a..fc070de2e 100644 --- a/quickfixj-core/src/main/java/quickfix/FieldException.java +++ b/quickfixj-core/src/main/java/quickfix/FieldException.java @@ -19,7 +19,7 @@ package quickfix; -public class FieldException extends RuntimeException { +public class FieldException extends RuntimeException implements HasFieldAndReason { private final int field; @@ -30,7 +30,7 @@ public FieldException(int sessionRejectReason) { } public FieldException(int sessionRejectReason, int field) { - super(SessionRejectReasonText.getMessage(sessionRejectReason) + ", field=" + field); + super(SessionRejectReasonText.getMessage(sessionRejectReason) + (field != -1 ? ", field=" + field : "")); this.sessionRejectReason = sessionRejectReason; this.field = field; } @@ -45,10 +45,12 @@ public boolean isFieldSpecified() { return field != -1; } + @Override public int getField() { return field; } + @Override public int getSessionRejectReason() { return sessionRejectReason; } diff --git a/quickfixj-core/src/main/java/quickfix/FieldMap.java b/quickfixj-core/src/main/java/quickfix/FieldMap.java index 4d26a1406..5a80be216 100644 --- a/quickfixj-core/src/main/java/quickfix/FieldMap.java +++ b/quickfixj-core/src/main/java/quickfix/FieldMap.java @@ -19,17 +19,6 @@ package quickfix; -import java.io.Serializable; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; - import quickfix.field.BeginString; import quickfix.field.BodyLength; import quickfix.field.CheckSum; @@ -43,6 +32,14 @@ import quickfix.field.converter.UtcTimeOnlyConverter; import quickfix.field.converter.UtcTimestampConverter; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.LocalDateTime; +import java.util.*; +import java.util.Map.Entry; + /** * Field container used by messages, groups, and composites. */ @@ -54,7 +51,7 @@ public abstract class FieldMap implements Serializable { private final TreeMap> fields; - private final TreeMap> groups = new TreeMap>(); + private final TreeMap> groups = new TreeMap<>(); /** * Constructs a FieldMap with the given field order. @@ -64,8 +61,7 @@ public abstract class FieldMap implements Serializable { */ protected FieldMap(int[] fieldOrder) { this.fieldOrder = fieldOrder; - fields = new TreeMap>( - fieldOrder != null ? new FieldOrderComparator() : null); + fields = new TreeMap<>(fieldOrder != null ? new FieldOrderComparator() : null); } protected FieldMap() { @@ -183,23 +179,31 @@ public void setDecimal(int field, BigDecimal value, int padding) { setField(new StringField(field, DecimalConverter.convert(value, padding))); } - public void setUtcTimeStamp(int field, Date value) { + public void setUtcTimeStamp(int field, LocalDateTime value) { setUtcTimeStamp(field, value, false); } - public void setUtcTimeStamp(int field, Date value, boolean includeMilliseconds) { - setField(new StringField(field, UtcTimestampConverter.convert(value, includeMilliseconds))); + public void setUtcTimeStamp(int field, LocalDateTime value, boolean includeMilliseconds) { + setField(new StringField(field, UtcTimestampConverter.convert(value, includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS))); + } + + public void setUtcTimeStamp(int field, LocalDateTime value, UtcTimestampPrecision precision) { + setField(new StringField(field, UtcTimestampConverter.convert(value, precision))); } - public void setUtcTimeOnly(int field, Date value) { + public void setUtcTimeOnly(int field, LocalTime value) { setUtcTimeOnly(field, value, false); } - public void setUtcTimeOnly(int field, Date value, boolean includeMillseconds) { - setField(new StringField(field, UtcTimeOnlyConverter.convert(value, includeMillseconds))); + public void setUtcTimeOnly(int field, LocalTime value, boolean includeMilliseconds) { + setField(new StringField(field, UtcTimeOnlyConverter.convert(value, includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS))); } - public void setUtcDateOnly(int field, Date value) { + public void setUtcTimeOnly(int field, LocalTime value, UtcTimestampPrecision precision) { + setField(new StringField(field, UtcTimeOnlyConverter.convert(value, precision))); + } + + public void setUtcDateOnly(int field, LocalDate value) { setField(new StringField(field, UtcDateOnlyConverter.convert(value))); } @@ -223,6 +227,15 @@ public String getString(int field) throws FieldNotFound { return getField(field).getObject(); } + public Optional getOptionalString(int field) { + final StringField f = (StringField) fields.get(field); + if (f == null) { + return Optional.empty(); + } else { + return Optional.of(f.getValue()); + } + } + public boolean getBoolean(int field) throws FieldNotFound { try { return BooleanConverter.convert(getString(field)); @@ -263,25 +276,25 @@ public BigDecimal getDecimal(int field) throws FieldNotFound { } } - public Date getUtcTimeStamp(int field) throws FieldNotFound { + public LocalDateTime getUtcTimeStamp(int field) throws FieldNotFound { try { - return UtcTimestampConverter.convert(getString(field)); + return UtcTimestampConverter.convertToLocalDateTime(getString(field)); } catch (final FieldConvertError e) { throw newIncorrectDataException(e, field); } } - public Date getUtcTimeOnly(int field) throws FieldNotFound { + public LocalTime getUtcTimeOnly(int field) throws FieldNotFound { try { - return UtcTimeOnlyConverter.convert(getString(field)); + return UtcTimeOnlyConverter.convertToLocalTime(getString(field)); } catch (final FieldConvertError e) { throw newIncorrectDataException(e, field); } } - public Date getUtcDateOnly(int field) throws FieldNotFound { + public LocalDate getUtcDateOnly(int field) throws FieldNotFound { try { - return UtcDateOnlyConverter.convert(getString(field)); + return UtcDateOnlyConverter.convertToLocalDate(getString(field)); } catch (final FieldConvertError e) { throw newIncorrectDataException(e, field); } @@ -319,11 +332,11 @@ public void setField(DecimalField field) { } public void setField(UtcTimeStampField field) { - setUtcTimeStamp(field.getField(), field.getValue(), field.showMilliseconds()); + setUtcTimeStamp(field.getField(), field.getValue(), field.getPrecision()); } public void setField(UtcTimeOnlyField field) { - setUtcTimeOnly(field.getField(), field.getValue(), field.showMilliseconds()); + setUtcTimeOnly(field.getField(), field.getValue(), field.getPrecision()); } public void setField(UtcDateOnlyField field) { @@ -412,7 +425,7 @@ protected void initializeFrom(FieldMap source) { fields.clear(); fields.putAll(source.fields); for (Entry> entry : source.groups.entrySet()) { - final List clones = new ArrayList(); + final List clones = new ArrayList<>(); for (final Group group : entry.getValue()) { final Group clone = new Group(group.getFieldTag(), group.delim(), group.getFieldOrder()); @@ -570,12 +583,7 @@ protected void setGroupCount(int countTag, int groupSize) { } public List getGroups(int field) { - List groupList = groups.get(field); - if (groupList == null) { - groupList = new ArrayList(); - groups.put(field, groupList); - } - return groupList; + return groups.computeIfAbsent(field, k -> new ArrayList<>()); } public Group getGroup(int num, Group group) throws FieldNotFound { diff --git a/quickfixj-core/src/main/java/quickfix/FieldNotFound.java b/quickfixj-core/src/main/java/quickfix/FieldNotFound.java index 1b08d3f10..63d8f2b3d 100644 --- a/quickfixj-core/src/main/java/quickfix/FieldNotFound.java +++ b/quickfixj-core/src/main/java/quickfix/FieldNotFound.java @@ -27,7 +27,7 @@ public class FieldNotFound extends Exception { public FieldNotFound(int field) { - super("Field [" + field + "] was not found in message."); + super("Field was not found in message, field=" + field); this.field = field; } diff --git a/quickfixj-core/src/main/java/quickfix/FieldType.java b/quickfixj-core/src/main/java/quickfix/FieldType.java index 802e4879d..767099543 100644 --- a/quickfixj-core/src/main/java/quickfix/FieldType.java +++ b/quickfixj-core/src/main/java/quickfix/FieldType.java @@ -19,7 +19,9 @@ package quickfix; -import java.util.Date; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.LocalDateTime; /** * A field type enum class. @@ -37,7 +39,7 @@ public enum FieldType { MULTIPLEVALUESTRING, MULTIPLESTRINGVALUE, // QFJ-881 EXCHANGE, - UTCTIMESTAMP(Date.class), + UTCTIMESTAMP(LocalDateTime.class), BOOLEAN(Boolean.class), LOCALMKTDATE, DATA, @@ -45,9 +47,9 @@ public enum FieldType { PRICEOFFSET(Double.class), MONTHYEAR, DAYOFMONTH(Integer.class), - UTCDATEONLY(Date.class), - UTCDATE(Date.class), - UTCTIMEONLY(Date.class), + UTCDATEONLY(LocalDate.class), + UTCDATE(LocalDate.class), + UTCTIMEONLY(LocalTime.class), TIME, NUMINGROUP(Integer.class), PERCENTAGE(Double.class), diff --git a/quickfixj-core/src/main/java/quickfix/FileLog.java b/quickfixj-core/src/main/java/quickfix/FileLog.java index de2a06454..ecd84fe5a 100644 --- a/quickfixj-core/src/main/java/quickfix/FileLog.java +++ b/quickfixj-core/src/main/java/quickfix/FileLog.java @@ -50,6 +50,8 @@ public class FileLog extends AbstractLog { private final String messagesFileName; private final String eventFileName; private boolean syncAfterWrite; + private final Object messagesLock = new Object(); + private final Object eventsLock = new Object(); private FileOutputStream messages; private FileOutputStream events; @@ -83,23 +85,25 @@ private void openLogStreams(boolean append) throws FileNotFoundException { } protected void logIncoming(String message) { - writeMessage(messages, message, false); + writeMessage(messages, messagesLock, message, false); } protected void logOutgoing(String message) { - writeMessage(messages, message, false); + writeMessage(messages, messagesLock, message, false); } - private void writeMessage(FileOutputStream stream, String message, boolean forceTimestamp) { + private void writeMessage(FileOutputStream stream, Object lock, String message, boolean forceTimestamp) { try { - if (forceTimestamp || includeTimestampForMessages) { - writeTimeStamp(stream); - } - stream.write(message.getBytes(CharsetSupport.getCharset())); - stream.write('\n'); - stream.flush(); - if (syncAfterWrite) { - stream.getFD().sync(); + synchronized(lock) { + if (forceTimestamp || includeTimestampForMessages) { + writeTimeStamp(stream); + } + stream.write(message.getBytes(CharsetSupport.getCharset())); + stream.write('\n'); + stream.flush(); + if (syncAfterWrite) { + stream.getFD().sync(); + } } } catch (IOException e) { // QFJ-459: no point trying to log the error in the file if we had an IOException @@ -110,11 +114,11 @@ private void writeMessage(FileOutputStream stream, String message, boolean force } public void onEvent(String message) { - writeMessage(events, message, true); + writeMessage(events, eventsLock, message, true); } public void onErrorEvent(String message) { - writeMessage(events, message, true); + writeMessage(events, eventsLock, message, true); } private void writeTimeStamp(OutputStream out) throws IOException { @@ -135,16 +139,6 @@ public void setSyncAfterWrite(boolean syncAfterWrite) { this.syncAfterWrite = syncAfterWrite; } - /** - * Closes the messages and events files. - * - * @deprecated Use close instead. - * @throws IOException - */ - public void closeFiles() throws IOException { - close(); - } - /** * Closed the messages and events files. * diff --git a/quickfixj-core/src/main/java/quickfix/FileLogFactory.java b/quickfixj-core/src/main/java/quickfix/FileLogFactory.java index 18f0dbd32..9c0cc850c 100644 --- a/quickfixj-core/src/main/java/quickfix/FileLogFactory.java +++ b/quickfixj-core/src/main/java/quickfix/FileLogFactory.java @@ -90,7 +90,4 @@ public Log create(SessionID sessionID) { } } - public Log create() { - throw new UnsupportedOperationException(); - } } diff --git a/quickfixj-core/src/main/java/quickfix/FileStore.java b/quickfixj-core/src/main/java/quickfix/FileStore.java index b965d05ac..20ec17f58 100644 --- a/quickfixj-core/src/main/java/quickfix/FileStore.java +++ b/quickfixj-core/src/main/java/quickfix/FileStore.java @@ -19,6 +19,9 @@ package quickfix; +import org.quickfixj.CharsetSupport; +import quickfix.field.converter.UtcTimestampConverter; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.Closeable; @@ -37,10 +40,6 @@ import java.util.Set; import java.util.TreeMap; -import org.quickfixj.CharsetSupport; - -import quickfix.field.converter.UtcTimestampConverter; - /** * File store implementation. THIS CLASS IS PUBLIC ONLY TO MAINTAIN * COMPATIBILITY WITH THE QUICKFIX JNI. IT SHOULD ONLY BE CREATED USING A @@ -77,7 +76,7 @@ public class FileStore implements MessageStore, Closeable { this.syncWrites = syncWrites; this.maxCachedMsgs = maxCachedMsgs; - messageIndex = maxCachedMsgs > 0 ? new TreeMap() : null; + messageIndex = maxCachedMsgs > 0 ? new TreeMap<>() : null; final String fullPath = new File(path == null ? "." : path).getAbsolutePath(); final String sessionName = FileUtil.sessionIdFileName(sessionID); @@ -98,10 +97,10 @@ public class FileStore implements MessageStore, Closeable { } void initialize(boolean deleteFiles) throws IOException { - close(); - if (deleteFiles) { - deleteFiles(); + closeAndDeleteFiles(); + } else { + close(); } String mode = READ_OPTION + WRITE_OPTION + (syncWrites ? SYNC_OPTION : NOSYNC_OPTION); @@ -124,16 +123,13 @@ private void initializeCache() throws IOException { private void initializeSessionCreateTime() throws IOException { final File sessionTimeFile = new File(sessionFileName); if (sessionTimeFile.exists() && sessionTimeFile.length() > 0) { - final DataInputStream sessionTimeInput = new DataInputStream(new BufferedInputStream( - new FileInputStream(sessionTimeFile))); - try { + try (DataInputStream sessionTimeInput = new DataInputStream(new BufferedInputStream( + new FileInputStream(sessionTimeFile)))) { final Calendar c = SystemTime.getUtcCalendar(UtcTimestampConverter .convert(sessionTimeInput.readUTF())); cache.setCreationTime(c); } catch (final Exception e) { throw new IOException(e.getMessage()); - } finally { - sessionTimeInput.close(); } } else { storeSessionTimeStamp(); @@ -141,14 +137,11 @@ private void initializeSessionCreateTime() throws IOException { } private void storeSessionTimeStamp() throws IOException { - final DataOutputStream sessionTimeOutput = new DataOutputStream(new BufferedOutputStream( - new FileOutputStream(sessionFileName, false))); - try { + try (DataOutputStream sessionTimeOutput = new DataOutputStream(new BufferedOutputStream( + new FileOutputStream(sessionFileName, false)))) { final Date date = SystemTime.getDate(); cache.setCreationTime(SystemTime.getUtcCalendar(date)); sessionTimeOutput.writeUTF(UtcTimestampConverter.convert(date, true)); - } finally { - sessionTimeOutput.close(); } } @@ -180,17 +173,14 @@ private void initializeMessageIndex() throws IOException { messageIndex.clear(); final File headerFile = new File(headerFileName); if (headerFile.exists()) { - final DataInputStream headerDataInputStream = new DataInputStream( - new BufferedInputStream(new FileInputStream(headerFile))); - try { + try (DataInputStream headerDataInputStream = new DataInputStream( + new BufferedInputStream(new FileInputStream(headerFile)))) { while (headerDataInputStream.available() > 0) { final int sequenceNumber = headerDataInputStream.readInt(); final long offset = headerDataInputStream.readLong(); final int size = headerDataInputStream.readInt(); updateMessageIndex(sequenceNumber, offset, size); } - } finally { - headerDataInputStream.close(); } } } @@ -229,7 +219,7 @@ private static void close(Closeable closeable) throws IOException { } } - public void deleteFiles() throws IOException { + public void closeAndDeleteFiles() throws IOException { close(); deleteFile(headerFileName); deleteFile(msgFileName); @@ -303,9 +293,9 @@ public void incrNextTargetMsgSeqNum() throws IOException { @Override public void get(int startSequence, int endSequence, Collection messages) throws IOException { - final Set uncachedOffsetMsgIds = new HashSet(); + final Set uncachedOffsetMsgIds = new HashSet<>(); // Use a treemap to make sure the messages are sorted by sequence num - final TreeMap messagesFound = new TreeMap(); + final TreeMap messagesFound = new TreeMap<>(); for (int i = startSequence; i <= endSequence; i++) { final String message = getMessage(i); if (message != null) { @@ -318,9 +308,8 @@ public void get(int startSequence, int endSequence, Collection messages) if (!uncachedOffsetMsgIds.isEmpty()) { // parse the header file to find missing messages final File headerFile = new File(headerFileName); - final DataInputStream headerDataInputStream = new DataInputStream( - new BufferedInputStream(new FileInputStream(headerFile))); - try { + try (DataInputStream headerDataInputStream = new DataInputStream( + new BufferedInputStream(new FileInputStream(headerFile)))) { while (!uncachedOffsetMsgIds.isEmpty() && headerDataInputStream.available() > 0) { final int sequenceNumber = headerDataInputStream.readInt(); final long offset = headerDataInputStream.readLong(); @@ -330,8 +319,6 @@ public void get(int startSequence, int endSequence, Collection messages) messagesFound.put(sequenceNumber, message); } } - } finally { - headerDataInputStream.close(); } } diff --git a/quickfixj-core/src/main/java/quickfix/FileUtil.java b/quickfixj-core/src/main/java/quickfix/FileUtil.java index e732ee562..c8159f778 100644 --- a/quickfixj-core/src/main/java/quickfix/FileUtil.java +++ b/quickfixj-core/src/main/java/quickfix/FileUtil.java @@ -24,7 +24,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.net.MalformedURLException; import java.net.URL; public class FileUtil { @@ -143,8 +142,6 @@ public static InputStream open(Class clazz, String name, Location... location case URL: try { in = new URL(name).openStream(); - } catch (MalformedURLException e) { - // ignore } catch (IOException e) { // ignore } diff --git a/quickfixj-core/src/main/java/quickfix/FixVersions.java b/quickfixj-core/src/main/java/quickfix/FixVersions.java index 16503b6e1..7cb7f9818 100644 --- a/quickfixj-core/src/main/java/quickfix/FixVersions.java +++ b/quickfixj-core/src/main/java/quickfix/FixVersions.java @@ -23,21 +23,21 @@ * Constants containing the BeginString field values for various FIX versions. */ public interface FixVersions { - public static final String BEGINSTRING_FIX40 = "FIX.4.0"; - public static final String BEGINSTRING_FIX41 = "FIX.4.1"; - public static final String BEGINSTRING_FIX42 = "FIX.4.2"; - public static final String BEGINSTRING_FIX43 = "FIX.4.3"; - public static final String BEGINSTRING_FIX44 = "FIX.4.4"; + String BEGINSTRING_FIX40 = "FIX.4.0"; + String BEGINSTRING_FIX41 = "FIX.4.1"; + String BEGINSTRING_FIX42 = "FIX.4.2"; + String BEGINSTRING_FIX43 = "FIX.4.3"; + String BEGINSTRING_FIX44 = "FIX.4.4"; /** * FIX 5.0 does not have a begin string. */ - public static final String FIX50 = "FIX.5.0"; - public static final String FIX50SP1 = "FIX.5.0SP1"; - public static final String FIX50SP2 = "FIX.5.0SP2"; + String FIX50 = "FIX.5.0"; + String FIX50SP1 = "FIX.5.0SP1"; + String FIX50SP2 = "FIX.5.0SP2"; // FIXT.x.x support - public static final String FIXT_SESSION_PREFIX = "FIXT."; - public static final String BEGINSTRING_FIXT11 = FIXT_SESSION_PREFIX + "1.1"; + String FIXT_SESSION_PREFIX = "FIXT."; + String BEGINSTRING_FIXT11 = FIXT_SESSION_PREFIX + "1.1"; } diff --git a/quickfixj-core/src/main/java/quickfix/Group.java b/quickfixj-core/src/main/java/quickfix/Group.java index 3600315f5..da4520da9 100644 --- a/quickfixj-core/src/main/java/quickfix/Group.java +++ b/quickfixj-core/src/main/java/quickfix/Group.java @@ -75,11 +75,4 @@ public int getFieldTag() { return field.getTag(); } - /** - * @deprecated Use getFieldTag - * @return the field's tag number - */ - public int field() { - return getFieldTag(); - } } diff --git a/quickfixj-core/src/main/java/quickfix/HasFieldAndReason.java b/quickfixj-core/src/main/java/quickfix/HasFieldAndReason.java new file mode 100644 index 000000000..7d30c60b7 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/HasFieldAndReason.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * 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; + +interface HasFieldAndReason { + + int getField(); + int getSessionRejectReason(); + String getMessage(); +} diff --git a/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java b/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java index 5068196cf..b1ae253f4 100644 --- a/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java +++ b/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java @@ -19,19 +19,22 @@ package quickfix; +import quickfix.field.SessionRejectReason; + /** * Field has a badly formatted value. (From the C++ API documentation.) */ -public class IncorrectDataFormat extends Exception { - public final int field; - public final String data; +public class IncorrectDataFormat extends Exception implements HasFieldAndReason { + private final int field; + private final String data; + private final int sessionRejectReason; /** * @param field the tag number with the incorrect data * @param data the incorrect data */ public IncorrectDataFormat(final int field, final String data) { - this(field, data, "Field [" + field + "] contains badly formatted data."); + this(field, data, SessionRejectReasonText.getMessage(SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE) + ", field=" + field); } /** @@ -51,10 +54,25 @@ public IncorrectDataFormat(final int field) { public IncorrectDataFormat(final String message) { this(0, null, message); } + + @Override + public int getSessionRejectReason() { + return sessionRejectReason; + } + @Override + public int getField() { + return field; + } + + public String getData() { + return data; + } + private IncorrectDataFormat(final int field, final String data, final String message) { super(message); this.field = field; this.data = data; + this.sessionRejectReason = SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE; } } diff --git a/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java b/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java index 1d55ea7c4..9558a9fbf 100644 --- a/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java +++ b/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java @@ -19,24 +19,35 @@ package quickfix; +import quickfix.field.SessionRejectReason; + /** * An exception thrown when a tags value is not valid according to the data dictionary. */ -public class IncorrectTagValue extends Exception { +public class IncorrectTagValue extends Exception implements HasFieldAndReason { + + private String value; + private final int field; + private final int sessionRejectReason; public IncorrectTagValue(int field) { - super("Field [" + field + "] contains an incorrect tag value."); + super(SessionRejectReasonText.getMessage(SessionRejectReason.VALUE_IS_INCORRECT) + ", field=" + field); this.field = field; + this.sessionRejectReason = SessionRejectReason.VALUE_IS_INCORRECT; } public IncorrectTagValue(int field, String value) { - super(); + super(SessionRejectReasonText.getMessage(SessionRejectReason.VALUE_IS_INCORRECT) + ", field=" + field + (value != null ? ", value=" + value : "")); this.field = field; this.value = value; + this.sessionRejectReason = SessionRejectReason.VALUE_IS_INCORRECT; } - public IncorrectTagValue(String s) { - super(s); + public IncorrectTagValue(int field, String value, String message) { + super(message); + this.field = field; + this.value = value; + this.sessionRejectReason = SessionRejectReason.VALUE_IS_INCORRECT; } @Override @@ -49,7 +60,14 @@ public String toString() { return str; } - public int field; + @Override + public int getField() { + return field; + } - public String value; + @Override + public int getSessionRejectReason() { + return sessionRejectReason; + } + } diff --git a/quickfixj-core/src/main/java/quickfix/Initiator.java b/quickfixj-core/src/main/java/quickfix/Initiator.java index 21899c60a..ffb39bf63 100644 --- a/quickfixj-core/src/main/java/quickfix/Initiator.java +++ b/quickfixj-core/src/main/java/quickfix/Initiator.java @@ -30,12 +30,12 @@ public interface Initiator extends Connector { * * @see quickfix.SessionFactory#SETTING_CONNECTION_TYPE */ - public static final String SETTING_RECONNECT_INTERVAL = "ReconnectInterval"; + String SETTING_RECONNECT_INTERVAL = "ReconnectInterval"; /** * Initiator setting for connection protocol (defaults to "tcp"). */ - public static final String SETTING_SOCKET_CONNECT_PROTOCOL = "SocketConnectProtocol"; + String SETTING_SOCKET_CONNECT_PROTOCOL = "SocketConnectProtocol"; /** * Initiator setting for connection host. Only valid when session connection @@ -43,7 +43,7 @@ public interface Initiator extends Connector { * * @see quickfix.SessionFactory#SETTING_CONNECTION_TYPE */ - public static final String SETTING_SOCKET_CONNECT_HOST = "SocketConnectHost"; + String SETTING_SOCKET_CONNECT_HOST = "SocketConnectHost"; /** * Initiator setting for connection port. Only valid when session connection @@ -51,7 +51,7 @@ public interface Initiator extends Connector { * * @see quickfix.SessionFactory#SETTING_CONNECTION_TYPE */ - public static final String SETTING_SOCKET_CONNECT_PORT = "SocketConnectPort"; + String SETTING_SOCKET_CONNECT_PORT = "SocketConnectPort"; /** * Initiator setting for local/bind host. Only valid when session connection @@ -59,7 +59,7 @@ public interface Initiator extends Connector { * * @see quickfix.SessionFactory#SETTING_CONNECTION_TYPE */ - public static final String SETTING_SOCKET_LOCAL_HOST = "SocketLocalHost"; + String SETTING_SOCKET_LOCAL_HOST = "SocketLocalHost"; /** * Initiator setting for local/bind port. Only valid when session connection @@ -67,5 +67,53 @@ public interface Initiator extends Connector { * * @see quickfix.SessionFactory#SETTING_CONNECTION_TYPE */ - public static final String SETTING_SOCKET_LOCAL_PORT = "SocketLocalPort"; + String SETTING_SOCKET_LOCAL_PORT = "SocketLocalPort"; + + /** + * Initiator setting for proxy type. Only valid when session connection + * type is "initiator". + */ + String SETTING_PROXY_TYPE = "ProxyType"; + + /** + * Initiator setting for proxy version. Only valid when session connection + * type is "initiator". - http 1.0 / 1.1 + */ + String SETTING_PROXY_VERSION = "ProxyVersion"; + + /** + * Initiator setting for proxy host. Only valid when session connection + * type is "initiator". + */ + String SETTING_PROXY_HOST = "ProxyHost"; + + /** + * Initiator setting for proxy port. Only valid when session connection + * type is "initiator". + */ + String SETTING_PROXY_PORT = "ProxyPort"; + + /** + * Initiator setting for proxy port. Only valid when session connection + * type is "initiator". + */ + String SETTING_PROXY_USER = "ProxyUser"; + + /** + * Initiator setting for proxy port. Only valid when session connection + * type is "initiator". + */ + String SETTING_PROXY_PASSWORD = "ProxyPassword"; + + /** + * Initiator setting for proxy domain. Only valid when session connection + * type is "initiator". + */ + String SETTING_PROXY_DOMAIN = "ProxyDomain"; + + /** + * Initiator setting for proxy workstation. Only valid when session connection + * type is "initiator". + */ + String SETTING_PROXY_WORKSTATION = "ProxyWorkstation"; } diff --git a/quickfixj-core/src/main/java/quickfix/InvalidMessage.java b/quickfixj-core/src/main/java/quickfix/InvalidMessage.java index aa6a92090..4b2b4cc3b 100644 --- a/quickfixj-core/src/main/java/quickfix/InvalidMessage.java +++ b/quickfixj-core/src/main/java/quickfix/InvalidMessage.java @@ -32,4 +32,9 @@ public InvalidMessage() { public InvalidMessage(String message) { super(message); } + + public InvalidMessage(String message, Throwable cause) { + super(message, cause); + } + } diff --git a/quickfixj-core/src/main/java/quickfix/JdbcLog.java b/quickfixj-core/src/main/java/quickfix/JdbcLog.java index 2dfefcda6..308e37fad 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcLog.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcLog.java @@ -19,9 +19,7 @@ package quickfix; -import static quickfix.JdbcSetting.*; -import static quickfix.JdbcUtil.*; - +import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -29,7 +27,15 @@ import java.util.HashMap; import java.util.Map; -import javax.sql.DataSource; +import static quickfix.JdbcSetting.SETTING_JDBC_LOG_HEARTBEATS; +import static quickfix.JdbcSetting.SETTING_JDBC_SESSION_ID_DEFAULT_PROPERTY_VALUE; +import static quickfix.JdbcSetting.SETTING_LOG_EVENT_TABLE; +import static quickfix.JdbcSetting.SETTING_LOG_INCOMING_TABLE; +import static quickfix.JdbcSetting.SETTING_LOG_OUTGOING_TABLE; +import static quickfix.JdbcUtil.determineSessionIdSupport; +import static quickfix.JdbcUtil.getIDColumns; +import static quickfix.JdbcUtil.getIDPlaceholders; +import static quickfix.JdbcUtil.getIDWhereClause; class JdbcLog extends AbstractLog { private static final String DEFAULT_MESSAGES_LOG_TABLE = "messages_log"; @@ -45,8 +51,8 @@ class JdbcLog extends AbstractLog { private Throwable recursiveException = null; - private final Map insertItemSqlCache = new HashMap(); - private final Map deleteItemsSqlCache = new HashMap(); + private final Map insertItemSqlCache = new HashMap<>(); + private final Map deleteItemsSqlCache = new HashMap<>(); public JdbcLog(SessionSettings settings, SessionID sessionID, DataSource ds) throws SQLException, ClassNotFoundException, ConfigError, FieldConvertError { @@ -55,22 +61,23 @@ public JdbcLog(SessionSettings settings, SessionID sessionID, DataSource ds) ? JdbcUtil.getDataSource(settings, sessionID) : ds; - logHeartbeats = !settings.isSetting(SETTING_JDBC_LOG_HEARTBEATS) || settings.getBool(SETTING_JDBC_LOG_HEARTBEATS); + logHeartbeats = !settings.isSetting(sessionID, SETTING_JDBC_LOG_HEARTBEATS) + || settings.getBool(sessionID, SETTING_JDBC_LOG_HEARTBEATS); setLogHeartbeats(logHeartbeats); - if (settings.isSetting(SETTING_LOG_OUTGOING_TABLE)) { + if (settings.isSetting(sessionID, SETTING_LOG_OUTGOING_TABLE)) { outgoingMessagesTableName = settings.getString(sessionID, SETTING_LOG_OUTGOING_TABLE); } else { outgoingMessagesTableName = DEFAULT_MESSAGES_LOG_TABLE; } - if (settings.isSetting(SETTING_LOG_INCOMING_TABLE)) { + if (settings.isSetting(sessionID, SETTING_LOG_INCOMING_TABLE)) { incomingMessagesTableName = settings.getString(sessionID, SETTING_LOG_INCOMING_TABLE); } else { incomingMessagesTableName = DEFAULT_MESSAGES_LOG_TABLE; } - if (settings.isSetting(SETTING_LOG_EVENT_TABLE)) { + if (settings.isSetting(sessionID, SETTING_LOG_EVENT_TABLE)) { eventTableName = settings.getString(sessionID, SETTING_LOG_EVENT_TABLE); } else { eventTableName = DEFAULT_EVENT_LOG_TABLE; diff --git a/quickfixj-core/src/main/java/quickfix/JdbcLogFactory.java b/quickfixj-core/src/main/java/quickfix/JdbcLogFactory.java index b5092ba85..78939176f 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcLogFactory.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcLogFactory.java @@ -57,10 +57,6 @@ protected SessionSettings getSettings() { return settings; } - public Log create() { - throw new UnsupportedOperationException(); - } - /** * Set a data source to be used by the JdbcLog to access * the database. diff --git a/quickfixj-core/src/main/java/quickfix/JdbcStore.java b/quickfixj-core/src/main/java/quickfix/JdbcStore.java index 46571aec1..1874d0953 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcStore.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcStore.java @@ -192,7 +192,7 @@ public void reset() throws IOException { setSessionIdParameters(updateTime, 4); updateTime.execute(); } catch (SQLException e) { - throw (IOException) new IOException(e.getMessage()).initCause(e); + throw new IOException(e.getMessage(), e); } finally { JdbcUtil.close(sessionID, deleteMessages); JdbcUtil.close(sessionID, updateTime); @@ -217,7 +217,7 @@ public void get(int startSequence, int endSequence, Collection messages) messages.add(message); } } catch (SQLException e) { - throw (IOException) new IOException(e.getMessage()).initCause(e); + throw new IOException(e.getMessage(), e); } finally { JdbcUtil.close(sessionID, rs); JdbcUtil.close(sessionID, query); @@ -247,7 +247,7 @@ public boolean set(int sequence, String message) throws IOException { boolean status = update.execute(); return !status && update.getUpdateCount() > 0; } catch (SQLException e) { - throw (IOException) new IOException(e.getMessage()).initCause(e); + throw new IOException(e.getMessage(), e); } finally { JdbcUtil.close(sessionID, update); } @@ -281,7 +281,7 @@ private void storeSequenceNumbers() throws IOException { setSessionIdParameters(update, 3); update.execute(); } catch (SQLException e) { - throw (IOException) new IOException(e.getMessage()).initCause(e); + throw new IOException(e.getMessage(), e); } finally { JdbcUtil.close(sessionID, update); JdbcUtil.close(sessionID, connection); @@ -292,7 +292,7 @@ public void refresh() throws IOException { try { loadCache(); } catch (SQLException e) { - throw (IOException) new IOException(e.getMessage()).initCause(e); + throw new IOException(e.getMessage(), e); } } diff --git a/quickfixj-core/src/main/java/quickfix/JdbcUtil.java b/quickfixj-core/src/main/java/quickfix/JdbcUtil.java index bd8f24b3f..14ffc259e 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcUtil.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcUtil.java @@ -19,6 +19,11 @@ package quickfix; +import org.logicalcobwebs.proxool.ProxoolDataSource; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; @@ -26,19 +31,15 @@ import java.sql.SQLException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; - -import javax.naming.InitialContext; -import javax.naming.NamingException; -import javax.sql.DataSource; - -import org.logicalcobwebs.proxool.ProxoolDataSource; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; class JdbcUtil { static final String CONNECTION_POOL_ALIAS = "quickfixj"; - private static final Map dataSources = new ConcurrentHashMap(); - private static int dataSourceCounter = 1; + private static final Map dataSources = new ConcurrentHashMap<>(); + private static final AtomicInteger dataSourceCounter = new AtomicInteger(); static DataSource getDataSource(SessionSettings settings, SessionID sessionID) throws ConfigError, FieldConvertError { @@ -82,35 +83,35 @@ static DataSource getDataSource(String jdbcDriver, String connectionURL, String } /** - * This is typically called from a single thread, but just in case we are synchronizing modification - * of the cache. The cache itself is thread safe. + * This is typically called from a single thread, but just in case we are using an atomic loading function + * to avoid the creation of two data sources simultaneously. The cache itself is thread safe. */ - static synchronized DataSource getDataSource(String jdbcDriver, String connectionURL, String user, String password, + static DataSource getDataSource(String jdbcDriver, String connectionURL, String user, String password, boolean cache, int maxConnCount, int simultaneousBuildThrottle, long maxActiveTime, int maxConnLifetime) { String key = jdbcDriver + "#" + connectionURL + "#" + user + "#" + password; ProxoolDataSource ds = cache ? dataSources.get(key) : null; if (ds == null) { - ds = new ProxoolDataSource(JdbcUtil.CONNECTION_POOL_ALIAS + "-" + dataSourceCounter++); - - ds.setDriver(jdbcDriver); - ds.setDriverUrl(connectionURL); - - // Bug in Proxool 0.9RC2. Must set both delegate properties and individual setters. :-( - ds.setDelegateProperties("user=" + user + "," - + (password != null && !"".equals(password) ? "password=" + password : "")); - ds.setUser(user); - ds.setPassword(password); - - ds.setMaximumActiveTime(maxActiveTime); - ds.setMaximumConnectionLifetime(maxConnLifetime); - ds.setMaximumConnectionCount(maxConnCount); - ds.setSimultaneousBuildThrottle(simultaneousBuildThrottle); - - if (cache) { - dataSources.put(key, ds); - } + final Function loadingFunction = dataSourceKey -> { + final ProxoolDataSource dataSource = new ProxoolDataSource(CONNECTION_POOL_ALIAS + "-" + dataSourceCounter.incrementAndGet()); + + dataSource.setDriver(jdbcDriver); + dataSource.setDriverUrl(connectionURL); + + // Bug in Proxool 0.9RC2. Must set both delegate properties and individual setters. :-( + dataSource.setDelegateProperties("user=" + user + "," + + (password != null && !"".equals(password) ? "password=" + password : "")); + dataSource.setUser(user); + dataSource.setPassword(password); + + dataSource.setMaximumActiveTime(maxActiveTime); + dataSource.setMaximumConnectionLifetime(maxConnLifetime); + dataSource.setMaximumConnectionCount(maxConnCount); + dataSource.setSimultaneousBuildThrottle(simultaneousBuildThrottle); + return dataSource; + }; + ds = cache ? dataSources.computeIfAbsent(key, loadingFunction) : loadingFunction.apply(key); } return ds; } @@ -146,24 +147,18 @@ static void close(SessionID sessionID, ResultSet rs) { } static boolean determineSessionIdSupport(DataSource dataSource, String tableName) throws SQLException { - Connection connection = dataSource.getConnection(); - try { + try (Connection connection = dataSource.getConnection()) { DatabaseMetaData metaData = connection.getMetaData(); String columnName = "sendersubid"; return isColumn(metaData, tableName.toUpperCase(), columnName.toUpperCase()) || isColumn(metaData, tableName, columnName); - } finally { - connection.close(); } } private static boolean isColumn(DatabaseMetaData metaData, String tableName, String columnName) throws SQLException { - ResultSet columns = metaData.getColumns(null, null, tableName, columnName); - try { + try (ResultSet columns = metaData.getColumns(null, null, tableName, columnName)) { return columns.next(); - } finally { - columns.close(); } } diff --git a/quickfixj-core/src/main/java/quickfix/ListenerSupport.java b/quickfixj-core/src/main/java/quickfix/ListenerSupport.java index 99041a21e..56146e1d6 100644 --- a/quickfixj-core/src/main/java/quickfix/ListenerSupport.java +++ b/quickfixj-core/src/main/java/quickfix/ListenerSupport.java @@ -26,7 +26,7 @@ import java.util.concurrent.CopyOnWriteArrayList; public class ListenerSupport { - private final List listeners = new CopyOnWriteArrayList(); + private final List listeners = new CopyOnWriteArrayList<>(); private final Object multicaster; public ListenerSupport(Class listenerClass) { diff --git a/quickfixj-core/src/main/java/quickfix/LogFactory.java b/quickfixj-core/src/main/java/quickfix/LogFactory.java index 9681f7e38..301a65e36 100644 --- a/quickfixj-core/src/main/java/quickfix/LogFactory.java +++ b/quickfixj-core/src/main/java/quickfix/LogFactory.java @@ -24,14 +24,6 @@ */ public interface LogFactory { - /** - * Create a log using default/global settings. - * - * @deprecated This method is not needed by QFJ and is generally not implemented. - * @return the log implementation - */ - Log create(); - /** * Create a log implementation. * diff --git a/quickfixj-core/src/main/java/quickfix/MemoryStore.java b/quickfixj-core/src/main/java/quickfix/MemoryStore.java index 553e3d6dc..f10205651 100644 --- a/quickfixj-core/src/main/java/quickfix/MemoryStore.java +++ b/quickfixj-core/src/main/java/quickfix/MemoryStore.java @@ -19,21 +19,21 @@ package quickfix; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; -import org.slf4j.LoggerFactory; - /** * In-memory message store implementation. * * @see quickfix.MemoryStoreFactory */ public class MemoryStore implements MessageStore { - private final HashMap messages = new HashMap(); + private final HashMap messages = new HashMap<>(); private int nextSenderMsgSeqNum; private int nextTargetMsgSeqNum; private SessionID sessionID; @@ -43,8 +43,9 @@ public MemoryStore() throws IOException { reset(); } - public MemoryStore(SessionID sessionID) { + public MemoryStore(SessionID sessionID) throws IOException { this.sessionID = sessionID; + reset(); } public void get(int startSequence, int endSequence, Collection messages) throws IOException { @@ -111,8 +112,8 @@ public void setNextTargetMsgSeqNum(int next) throws IOException { public void refresh() throws IOException { // IOException is declared to maintain strict compatibility with QF JNI final String text = "memory store does not support refresh!"; - if (sessionID != null) { - Session session = Session.lookupSession(sessionID); + final Session session = sessionID != null ? Session.lookupSession(sessionID) : null; + if (session != null) { session.getLog().onErrorEvent("ERROR: " + text); } else { LoggerFactory.getLogger(MemoryStore.class).error(text); diff --git a/quickfixj-core/src/main/java/quickfix/MemoryStoreFactory.java b/quickfixj-core/src/main/java/quickfix/MemoryStoreFactory.java index 890365166..09b670b73 100644 --- a/quickfixj-core/src/main/java/quickfix/MemoryStoreFactory.java +++ b/quickfixj-core/src/main/java/quickfix/MemoryStoreFactory.java @@ -30,7 +30,7 @@ public class MemoryStoreFactory implements MessageStoreFactory { public MessageStore create(SessionID sessionID) { try { - return new MemoryStore(); + return new MemoryStore(sessionID); } catch (IOException e) { throw new RuntimeError(e); } diff --git a/quickfixj-core/src/main/java/quickfix/Message.java b/quickfixj-core/src/main/java/quickfix/Message.java index d39e8592b..f227808d6 100644 --- a/quickfixj-core/src/main/java/quickfix/Message.java +++ b/quickfixj-core/src/main/java/quickfix/Message.java @@ -23,19 +23,18 @@ import java.text.DecimalFormat; import java.util.Iterator; import java.util.List; - import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; - import org.quickfixj.CharsetSupport; import org.w3c.dom.CDATASection; import org.w3c.dom.Document; import org.w3c.dom.Element; +import quickfix.field.ApplExtID; import quickfix.field.ApplVerID; import quickfix.field.BeginString; import quickfix.field.BodyLength; @@ -79,8 +78,7 @@ public class Message extends FieldMap { protected Header header = new Header(); protected Trailer trailer = new Trailer(); - // @GuardedBy("this") - private FieldException exception; + private volatile FieldException exception; public Message() { // empty @@ -115,9 +113,7 @@ public Object clone() { try { final Message message = getClass().newInstance(); return cloneTo(message); - } catch (final InstantiationException e) { - throw new RuntimeException(e); - } catch (final IllegalAccessException e) { + } catch (final InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } @@ -129,23 +125,146 @@ private Object cloneTo(Message message) { return message; } + private static final class Context { + private final BodyLength bodyLength = new BodyLength(100); + private final CheckSum checkSum = new CheckSum("000"); + private final StringBuilder stringBuilder = new StringBuilder(1024); + } + + private static final ThreadLocal stringContexts = new ThreadLocal() { + @Override + protected Context initialValue() { + return new Context(); + } + }; + + protected static boolean IS_STRING_EQUIVALENT = CharsetSupport.isStringEquivalent(CharsetSupport.getCharsetInstance()); + /** * 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() { - final int bodyLength = bodyLength(); - header.setInt(BodyLength.FIELD, bodyLength); - trailer.setString(CheckSum.FIELD, checksum()); - - final StringBuilder sb = new StringBuilder(bodyLength); - header.calculateString(sb, null, null); - calculateString(sb, null, null); - trailer.calculateString(sb, null, null); + Context context = stringContexts.get(); + if (IS_STRING_EQUIVALENT) { // length & checksum can easily be calculated after message is built + header.setField(context.bodyLength); + trailer.setField(context.checkSum); + } else { + header.setInt(BodyLength.FIELD, bodyLength()); + trailer.setString(CheckSum.FIELD, checksum()); + } + StringBuilder stringBuilder = context.stringBuilder; + try { + header.calculateString(stringBuilder, null, null); + calculateString(stringBuilder, null, null); + trailer.calculateString(stringBuilder, null, null); + if (IS_STRING_EQUIVALENT) { + setBodyLength(stringBuilder); + setChecksum(stringBuilder); + } + return stringBuilder.toString(); + } finally { + stringBuilder.setLength(0); + } + } + + 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 static void setBodyLength(StringBuilder stringBuilder) { + int bodyLengthIndex = indexOf(stringBuilder, BODY_LENGTH_FIELD, 0); + int sohIndex = indexOf(stringBuilder, SOH, bodyLengthIndex + 1); + int checkSumIndex = lastIndexOf(stringBuilder, 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 = lastIndexOf(stringBuilder, 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); + } + + // return index of a string in a stringbuilder without performing allocations + private static int indexOf(StringBuilder source, String target, int fromIndex) { + if (fromIndex >= source.length()) + return (target.length() == 0 ? source.length() : -1); + if (fromIndex < 0) + fromIndex = 0; + if (target.length() == 0) + return fromIndex; + char first = target.charAt(0); + int max = source.length() - target.length(); + for (int i = fromIndex; i <= max; i++) { + if (source.charAt(i) != first) + while (++i <= max && source.charAt(i) != first); + if (i <= max) { + int j = i + 1; + int end = j + target.length() - 1; + for (int k = 1; j < end && source.charAt(j) + == target.charAt(k); j++, k++); + if (j == end) + return i; + } + } + return -1; + } + + // return last index of a string in a stringbuilder without performing allocations + private static int lastIndexOf(StringBuilder source, String target) { + int rightIndex = source.length() - target.length(); + int fromIndex = source.length(); + if (fromIndex > rightIndex) + fromIndex = rightIndex; + if (target.length() == 0) + return fromIndex; + int strLastIndex = target.length() - 1; + char strLastChar = target.charAt(strLastIndex); + int min = target.length() - 1; + int i = min + fromIndex; + startSearchForLastChar: + while (true) { + while (i >= min && source.charAt(i) != strLastChar) + i--; + if (i < min) + return -1; + int j = i - 1; + int start = j - (target.length() - 1); + int k = strLastIndex - 1; + while (j > start) + if (source.charAt(j--) != target.charAt(k--)) { + i--; + continue startSearchForLastChar; + } + return start + 1; + } + } - return sb.toString(); + /** + * 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(). + * 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. + */ + public String toRawString() { + return messageData; } public int bodyLength() { @@ -514,7 +633,7 @@ && isNextField(dd, header, BodyLength.FIELD) header.setField(field); if (dd != null && dd.isGroup(DataDictionary.HEADER_ID, field.getField())) { - parseGroup(DataDictionary.HEADER_ID, field, dd, header); + parseGroup(DataDictionary.HEADER_ID, field, dd, dd, header, doValidation); } field = extractField(dd, header); @@ -553,7 +672,7 @@ private void parseBody(DataDictionary dd, boolean doValidation) throws InvalidMe setField(header, field); // Group case if (dd != null && dd.isGroup(DataDictionary.HEADER_ID, field.getField())) { - parseGroup(DataDictionary.HEADER_ID, field, dd, header); + parseGroup(DataDictionary.HEADER_ID, field, dd, dd, header, doValidation); } if (doValidation && dd != null && dd.isCheckFieldsOutOfOrder()) throw new FieldException(SessionRejectReason.TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER, @@ -562,7 +681,7 @@ private void parseBody(DataDictionary dd, boolean doValidation) throws InvalidMe setField(this, field); // Group case if (dd != null && dd.isGroup(getMsgType(), field.getField())) { - parseGroup(getMsgType(), field, dd, this); + parseGroup(getMsgType(), field, dd, dd, this, doValidation); } } @@ -577,14 +696,20 @@ private void setField(FieldMap fields, StringField field) { fields.setField(field); } - private void parseGroup(String msgType, StringField field, DataDictionary dd, FieldMap parent) + private void parseGroup(String msgType, StringField field, DataDictionary dd, DataDictionary parentDD, FieldMap parent, boolean doValidation) throws InvalidMessage { final DataDictionary.GroupInfo rg = dd.getGroup(msgType, field.getField()); final DataDictionary groupDataDictionary = rg.getDataDictionary(); final int[] fieldOrder = groupDataDictionary.getOrderedFields(); int previousOffset = -1; final int groupCountTag = field.getField(); - final int declaredGroupCount = Integer.parseInt(field.getValue()); + // QFJ-533 + int declaredGroupCount = 0; + try { + declaredGroupCount = Integer.parseInt(field.getValue()); + } catch (final NumberFormatException e) { + throw new InvalidMessage("Repeating group count requires an Integer but found: " + field.getValue(), e); + } parent.setField(groupCountTag, field); final int firstField = rg.getDelimiterField(); boolean firstFieldFound = false; @@ -598,33 +723,28 @@ private void parseGroup(String msgType, StringField field, DataDictionary dd, Fi } int tag = field.getTag(); if (tag == firstField) { - if (group != null) { - parent.addGroupRef(group); - } + 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, group); + parseGroup(msgType, field, groupDataDictionary, parentDD, group, doValidation); } } else if (groupDataDictionary.isGroup(msgType, tag)) { - if (!firstFieldFound) { - throw new InvalidMessage("The group " + groupCountTag - + " must set the delimiter field " + firstField + " in " + messageData); - } - parseGroup(msgType, field, groupDataDictionary, group); + // QFJ-934: message should be rejected and not ignored when first field not found + checkFirstFieldFound(firstFieldFound, groupCountTag, firstField, tag); + parseGroup(msgType, field, groupDataDictionary, parentDD, group, doValidation); } else if (groupDataDictionary.isField(tag)) { - if (!firstFieldFound) { - throw new FieldException( - SessionRejectReason.REPEATING_GROUP_FIELDS_OUT_OF_ORDER, tag); - } - + checkFirstFieldFound(firstFieldFound, groupCountTag, firstField, tag); if (fieldOrder != null && dd.isCheckUnorderedGroupFields()) { final int offset = indexOf(tag, fieldOrder); if (offset > -1) { if (offset <= previousOffset) { + // QFJ-792: add what we've already got and leave the rest to the validation (if enabled) + group.setField(field); + addGroupRefToParent(group, parent); throw new FieldException( SessionRejectReason.REPEATING_GROUP_FIELDS_OUT_OF_ORDER, tag); } @@ -633,16 +753,49 @@ private void parseGroup(String msgType, StringField field, DataDictionary dd, Fi } group.setField(field); } else { + // QFJ-169/QFJ-791: handle unknown repeating group fields in the body + if (!isTrailerField(tag) && !(DataDictionary.HEADER_ID.equals(msgType))) { + if (checkFieldValidation(parent, parentDD, field, msgType, doValidation, group)) { + continue; + } + } pushBack(field); inGroupParse = false; } } // 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); + } + + private void addGroupRefToParent(Group group, FieldMap parent) { if (group != null) { parent.addGroupRef(group); } - // For later validation that the group size matches the parsed group count - parent.setGroupCount(groupCountTag, declaredGroupCount); + } + + 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 { + 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); + if (fail) { + throw new FieldException(SessionRejectReason.TAG_NOT_DEFINED_FOR_THIS_MESSAGE_TYPE, field.getTag()); + } + } + group.setField(field); + return true; + } + return false; } private void parseTrailer(DataDictionary dd) throws InvalidMessage { @@ -692,6 +845,7 @@ static boolean isHeaderField(int field) { case OnBehalfOfSendingTime.FIELD: case ApplVerID.FIELD: case CstmApplVerID.FIELD: + case ApplExtID.FIELD: case NoHops.FIELD: return true; default: @@ -769,7 +923,7 @@ private StringField extractField(DataDictionary dataDictionary, FieldMap fields) try { fieldLength = fields.getInt(lengthField); } catch (final FieldNotFound e) { - throw new InvalidMessage("Tag " + e.field + " not found in " + messageData); + throw new InvalidMessage("Did not find length field " + e.field + " required to parse data field " + tag + " in " + messageData); } // since length is in bytes but data is a string, and it may also contain an SOH, @@ -794,11 +948,11 @@ private StringField extractField(DataDictionary dataDictionary, FieldMap fields) * * @return flag indicating whether the message has a valid structure */ - synchronized boolean hasValidStructure() { + boolean hasValidStructure() { return exception == null; } - public synchronized FieldException getException() { + public FieldException getException() { return exception; } @@ -808,7 +962,7 @@ public synchronized FieldException getException() { * * @return the first invalid tag */ - synchronized int getInvalidTag() { + int getInvalidTag() { return exception != null ? exception.getField() : 0; } @@ -827,4 +981,5 @@ public static MsgType identifyType(String message) throws MessageParseError { } } + } diff --git a/quickfixj-core/src/main/java/quickfix/MessageCracker.java b/quickfixj-core/src/main/java/quickfix/MessageCracker.java index 3874d6ea0..28b65dac1 100644 --- a/quickfixj-core/src/main/java/quickfix/MessageCracker.java +++ b/quickfixj-core/src/main/java/quickfix/MessageCracker.java @@ -34,7 +34,7 @@ * type-safe onMessage methods. */ public class MessageCracker { - private final Map, Invoker> invokers = new HashMap, Invoker>(); + private final Map, Invoker> invokers = new HashMap<>(); @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @@ -128,11 +128,7 @@ public void crack(quickfix.Message message, SessionID sessionID) throws Unsuppor } catch (InvocationTargetException ite) { try { throw ite.getTargetException(); - } catch (UnsupportedMessageType e) { - throw e; - } catch (FieldNotFound e) { - throw e; - } catch (IncorrectTagValue e) { + } catch (UnsupportedMessageType | IncorrectTagValue | FieldNotFound e) { throw e; } catch (Throwable t) { propagate(t); diff --git a/quickfixj-core/src/main/java/quickfix/MessageFactory.java b/quickfixj-core/src/main/java/quickfix/MessageFactory.java index eb90d23f3..cf734f831 100644 --- a/quickfixj-core/src/main/java/quickfix/MessageFactory.java +++ b/quickfixj-core/src/main/java/quickfix/MessageFactory.java @@ -19,6 +19,8 @@ package quickfix; +import quickfix.field.ApplVerID; + /** * Used by a Session to create a Message. * @@ -35,6 +37,18 @@ public interface MessageFactory { */ Message create(String beginString, String msgType); + /** + * Creates a message for a specified type, FIX version, and ApplVerID. + * + * @param beginString the FIX version (for example, "FIX.4.2") + * @param applVerID the ApplVerID (for example "6" for FIX44) + * @param msgType the FIX message type (for example, "D" for an order) + * @return a message instance + */ + default Message create(String beginString, ApplVerID applVerID, String msgType) { + return create(beginString, msgType); + } + /** * Creates a group for the specified parent message type and * for the fields with the corresponding field ID @@ -50,5 +64,5 @@ public interface MessageFactory { * @param correspondingFieldID the fieldID of the field in the group * @return group, or null if the group can't be created. */ - public Group create(String beginString, String msgType, int correspondingFieldID); + Group create(String beginString, String msgType, int correspondingFieldID); } diff --git a/quickfixj-core/src/main/java/quickfix/MessageUtils.java b/quickfixj-core/src/main/java/quickfix/MessageUtils.java index d22766945..01ade452a 100644 --- a/quickfixj-core/src/main/java/quickfix/MessageUtils.java +++ b/quickfixj-core/src/main/java/quickfix/MessageUtils.java @@ -142,7 +142,7 @@ public static Message parse(Session session, String messageString) throws Invali final DataDictionary applicationDataDictionary = ddProvider == null ? null : ddProvider .getApplicationDataDictionary(applVerID); - final quickfix.Message message = messageFactory.create(beginString, msgType); + final quickfix.Message message = messageFactory.create(beginString, applVerID, msgType); final DataDictionary payloadDictionary = MessageUtils.isAdminMessage(msgType) ? sessionDataDictionary : applicationDataDictionary; diff --git a/quickfixj-core/src/main/java/quickfix/NoopStore.java b/quickfixj-core/src/main/java/quickfix/NoopStore.java new file mode 100644 index 000000000..ddc33b54c --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/NoopStore.java @@ -0,0 +1,80 @@ +/* + ****************************************************************************** + * 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.util.Collection; +import java.util.Date; + +/** + * No-op message store implementation. + * + * @see quickfix.MemoryStoreFactory + */ +public class NoopStore implements MessageStore { + + private Date creationTime = new Date(); + private int nextSenderMsgSeqNum = 1; + private int nextTargetMsgSeqNum = 1; + + public void get(int startSequence, int endSequence, Collection messages) { + } + + public Date getCreationTime() { + return creationTime; + } + + public int getNextSenderMsgSeqNum() { + return nextSenderMsgSeqNum; + } + + public int getNextTargetMsgSeqNum() { + return nextTargetMsgSeqNum; + } + + public void incrNextSenderMsgSeqNum() { + nextSenderMsgSeqNum++; + } + + public void incrNextTargetMsgSeqNum() { + nextTargetMsgSeqNum++; + } + + public void reset() { + creationTime = new Date(); + nextSenderMsgSeqNum = 1; + nextTargetMsgSeqNum = 1; + } + + public boolean set(int sequence, String message) { + return true; + } + + public void setNextSenderMsgSeqNum(int next) { + nextSenderMsgSeqNum = next; + } + + public void setNextTargetMsgSeqNum(int next) { + nextTargetMsgSeqNum = next; + } + + public void refresh() { + } +} diff --git a/quickfixj-core/src/main/java/quickfix/NoopStoreFactory.java b/quickfixj-core/src/main/java/quickfix/NoopStoreFactory.java new file mode 100644 index 000000000..2ec3cec08 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/NoopStoreFactory.java @@ -0,0 +1,34 @@ +/* + ****************************************************************************** + * 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; + +/** + * Creates a no-op message store. + * + * @see MessageStore + */ +public final class NoopStoreFactory implements MessageStoreFactory { + + @Override + public MessageStore create(SessionID sessionID) { + return new NoopStore(); + } +} diff --git a/quickfixj-core/src/main/java/quickfix/NumbersCache.java b/quickfixj-core/src/main/java/quickfix/NumbersCache.java new file mode 100644 index 000000000..1a3725884 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/NumbersCache.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * 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.util.ArrayList; + +/** + * A cache for commonly used strings representing numbers. + * Holds values from 0 to 99999. + */ +public final class NumbersCache { + + private static final int LITTLE_NUMBERS_LENGTH = 100000; + private static final ArrayList LITTLE_NUMBERS; + + static { + LITTLE_NUMBERS = new ArrayList<>(LITTLE_NUMBERS_LENGTH); + for (int i = 0; i < LITTLE_NUMBERS_LENGTH; i++) + LITTLE_NUMBERS.add(Integer.toString(i)); + } + + /** + * Get the String representing the given number + * + * @param i the long to convert + * @return the String representing the long + */ + public static String get(int i) { + if (i < LITTLE_NUMBERS_LENGTH) + return LITTLE_NUMBERS.get(i); + return String.valueOf(i); + } + + /** + * Get the string representing the given double if it's an integer + * + * @param d the double to convert + * @return the String representing the double or null if the double is not an integer + */ + public static String get(double d) { + long l = (long)d; + if (d == (double)l) + return get(l); + return null; + } +} diff --git a/quickfixj-core/src/main/java/quickfix/SLF4JLog.java b/quickfixj-core/src/main/java/quickfix/SLF4JLog.java index 33fa52cd9..58c1a0798 100644 --- a/quickfixj-core/src/main/java/quickfix/SLF4JLog.java +++ b/quickfixj-core/src/main/java/quickfix/SLF4JLog.java @@ -47,26 +47,24 @@ public class SLF4JLog extends AbstractLog { private final Logger outgoingMsgLog; - private final String logPrefix; - private final String callerFQCN; public SLF4JLog(SessionID sessionID, String eventCategory, String errorEventCategory, - String incomingMsgCategory, String outgoingMsgCategory, boolean prependSessionID, - boolean logHeartbeats, String inCallerFQCN) { + String incomingMsgCategory, String outgoingMsgCategory, boolean prependSessionID, + boolean logHeartbeats, String inCallerFQCN) { setLogHeartbeats(logHeartbeats); - logPrefix = prependSessionID ? (sessionID + ": ") : null; - eventLog = getLogger(sessionID, eventCategory, DEFAULT_EVENT_CATEGORY); - errorEventLog = getLogger(sessionID, errorEventCategory, DEFAULT_ERROR_EVENT_CATEGORY); - incomingMsgLog = getLogger(sessionID, incomingMsgCategory, DEFAULT_INCOMING_MSG_CATEGORY); - outgoingMsgLog = getLogger(sessionID, outgoingMsgCategory, DEFAULT_OUTGOING_MSG_CATEGORY); + String logPrefix = prependSessionID ? (sessionID + ": ") : ""; + eventLog = getLogger(sessionID, eventCategory, DEFAULT_EVENT_CATEGORY, logPrefix); + errorEventLog = getLogger(sessionID, errorEventCategory, DEFAULT_ERROR_EVENT_CATEGORY, logPrefix); + incomingMsgLog = getLogger(sessionID, incomingMsgCategory, DEFAULT_INCOMING_MSG_CATEGORY, logPrefix); + outgoingMsgLog = getLogger(sessionID, outgoingMsgCategory, DEFAULT_OUTGOING_MSG_CATEGORY, logPrefix); callerFQCN = inCallerFQCN; } - private Logger getLogger(SessionID sessionID, String category, String defaultCategory) { - return LoggerFactory.getLogger(category != null + private Logger getLogger(SessionID sessionID, String category, String defaultCategory, String logPrefix) { + return LoggerFactory.getLogger((category != null ? substituteVariables(sessionID, category) - : defaultCategory); + : defaultCategory) + logPrefix); } private static final String FIX_MAJOR_VERSION_VAR = "\\$\\{fixMajorVersion}"; @@ -134,23 +132,23 @@ protected void logOutgoing(String message) { */ protected void log(org.slf4j.Logger log, String text) { if (log.isInfoEnabled()) { - final String message = logPrefix != null ? (logPrefix + text) : text; if (log instanceof LocationAwareLogger) { final LocationAwareLogger la = (LocationAwareLogger) log; - la.log(null, callerFQCN, LocationAwareLogger.INFO_INT, message, null, null); + la.log(null, callerFQCN, LocationAwareLogger.INFO_INT, text, null, null); } else { - log.info(message); + log.info(text); } } } protected void logError(org.slf4j.Logger log, String text) { - final String message = logPrefix != null ? (logPrefix + text) : text; - log.error(message); + log.error(text); } + private final String clearString = "Log clear operation is not supported: " + getClass().getName(); + public void clear() { - onEvent("Log clear operation is not supported: " + getClass().getName()); + onEvent(clearString); } } diff --git a/quickfixj-core/src/main/java/quickfix/SLF4JLogFactory.java b/quickfixj-core/src/main/java/quickfix/SLF4JLogFactory.java index 635d7447d..104b87c64 100644 --- a/quickfixj-core/src/main/java/quickfix/SLF4JLogFactory.java +++ b/quickfixj-core/src/main/java/quickfix/SLF4JLogFactory.java @@ -104,8 +104,4 @@ public Log create(SessionID sessionID, String callerFQCN) { prependSessionID, logHeartbeats, callerFQCN); } - public Log create() { - throw new UnsupportedOperationException(); - } - } diff --git a/quickfixj-core/src/main/java/quickfix/ScreenLogFactory.java b/quickfixj-core/src/main/java/quickfix/ScreenLogFactory.java index e2b8dec63..b6d97e1b9 100644 --- a/quickfixj-core/src/main/java/quickfix/ScreenLogFactory.java +++ b/quickfixj-core/src/main/java/quickfix/ScreenLogFactory.java @@ -126,9 +126,7 @@ public Log create(SessionID sessionID) { includeMillis = getBooleanSetting(sessionID, ScreenLogFactory.SETTING_INCLUDE_MILLIS_IN_TIMESTAMP, false); return new ScreenLog(incoming, outgoing, events, heartBeats, includeMillis, sessionID, System.out); - } catch (FieldConvertError e) { - throw new RuntimeError(e); - } catch (ConfigError e) { + } catch (FieldConvertError | ConfigError e) { throw new RuntimeError(e); } } @@ -141,8 +139,4 @@ private boolean getBooleanSetting(SessionID sessionID, String key, boolean incom return incoming; } - public Log create() { - throw new UnsupportedOperationException(); - } - } diff --git a/quickfixj-core/src/main/java/quickfix/Session.java b/quickfixj-core/src/main/java/quickfix/Session.java index b30c87652..23f7665ca 100644 --- a/quickfixj-core/src/main/java/quickfix/Session.java +++ b/quickfixj-core/src/main/java/quickfix/Session.java @@ -19,23 +19,8 @@ package quickfix; -import static quickfix.LogUtil.logThrowable; - -import java.io.Closeable; -import java.io.IOException; -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import quickfix.Message.Header; import quickfix.SessionState.ResendRange; import quickfix.field.ApplVerID; @@ -71,6 +56,22 @@ import quickfix.field.Text; import quickfix.mina.EventHandlingStrategy; +import java.io.Closeable; +import java.io.IOException; +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.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static quickfix.LogUtil.logThrowable; + /** * The Session is the primary FIX abstraction for message communication. *

    @@ -146,9 +147,14 @@ public class Session implements Closeable { */ public static final String SETTING_END_TIME = "EndTime"; + /** + * Session scheduling setting to specify active days of the week. + */ + public static final String SETTING_WEEKDAYS = "Weekdays"; + /** * Session setting to indicate whether a data dictionary should be used. If - * a data dictionary is not used then message validation is not possble. + * a data dictionary is not used then message validation is not possible. */ public static final String SETTING_USE_DATA_DICTIONARY = "UseDataDictionary"; @@ -253,10 +259,11 @@ public class Session implements Closeable { public static final String SETTING_DISCONNECT_ON_ERROR = "DisconnectOnError"; /** - * Session setting to enable milliseconds in message timestamps. Valid - * values are "Y" or "N". Default is "Y". Only valid for FIX version >= 4.2. + * Session setting to control precision in message timestamps. + * Valid values are "SECONDS", "MILLIS", "MICROS", "NANOS". Default is "MILLIS". + * Only valid for FIX version >= 4.2. */ - public static final String SETTING_MILLISECONDS_IN_TIMESTAMP = "MillisecondsInTimeStamp"; + public static final String SETTING_TIMESTAMP_PRECISION = "TimeStampPrecision"; /** * Controls validation of user-defined fields. @@ -344,7 +351,7 @@ public class Session implements Closeable { public static final String SETTING_MAX_SCHEDULED_WRITE_REQUESTS = "MaxScheduledWriteRequests"; - private static final ConcurrentMap sessions = new ConcurrentHashMap(); + private static final ConcurrentMap sessions = new ConcurrentHashMap<>(); private final Application application; private final SessionID sessionID; @@ -375,7 +382,7 @@ public class Session implements Closeable { private final boolean resetOnDisconnect; private final boolean resetOnError; private final boolean disconnectOnError; - private final boolean millisecondsInTimeStamp; + private final UtcTimestampPrecision timestampPrecision; private final boolean refreshMessageStoreAtLogon; private final boolean redundantResentRequestsAllowed; private final boolean persistMessages; @@ -392,30 +399,39 @@ public class Session implements Closeable { private int maxScheduledWriteRequests = 0; private final AtomicBoolean isResetting = new AtomicBoolean(); + private final AtomicBoolean isResettingState = new AtomicBoolean(); private final ListenerSupport stateListeners = new ListenerSupport(SessionStateListener.class); private final SessionStateListener stateListener = (SessionStateListener) stateListeners .getMulticaster(); - private final AtomicReference targetDefaultApplVerID = new AtomicReference(); + private final AtomicReference targetDefaultApplVerID = new AtomicReference<>(); private final DefaultApplVerID senderDefaultApplVerID; private boolean validateSequenceNumbers = true; private boolean validateIncomingMessage = true; private final int[] logonIntervals; private final Set allowedRemoteAddresses; - + public static final int DEFAULT_MAX_LATENCY = 120; public static final int DEFAULT_RESEND_RANGE_CHUNK_SIZE = 0; // no resend range public static final double DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER = 0.5; private static final String ENCOUNTERED_END_OF_STREAM = "Encountered END_OF_STREAM"; - protected final static Logger log = LoggerFactory.getLogger(Session.class); + + 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; + private static final String BAD_ORIG_TIME_TEXT = new FieldException(BAD_TIME_REJ_REASON, OrigSendingTime.FIELD).getMessage(); + private static final String BAD_TIME_TEXT = new FieldException(BAD_TIME_REJ_REASON, SendingTime.FIELD).getMessage(); + + protected static final Logger LOG = LoggerFactory.getLogger(Session.class); + Session(Application application, MessageStoreFactory messageStoreFactory, SessionID sessionID, DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule, LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval) { this(application, messageStoreFactory, sessionID, dataDictionaryProvider, sessionSchedule, - logFactory, messageFactory, heartbeatInterval, true, DEFAULT_MAX_LATENCY, true, + 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, true, false, true, false, null, true, DEFAULT_RESEND_RANGE_CHUNK_SIZE, false, false); @@ -424,7 +440,7 @@ public class Session implements Closeable { Session(Application application, MessageStoreFactory messageStoreFactory, SessionID sessionID, DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule, LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval, - boolean checkLatency, int maxLatency, boolean millisecondsInTimeStamp, + boolean checkLatency, int maxLatency, UtcTimestampPrecision timestampPrecision, boolean resetOnLogon, boolean resetOnLogout, boolean resetOnDisconnect, boolean refreshMessageStoreAtLogon, boolean checkCompID, boolean redundantResentRequestsAllowed, boolean persistMessages, @@ -444,7 +460,7 @@ public class Session implements Closeable { this.resetOnLogon = resetOnLogon; this.resetOnLogout = resetOnLogout; this.resetOnDisconnect = resetOnDisconnect; - this.millisecondsInTimeStamp = millisecondsInTimeStamp; + this.timestampPrecision = timestampPrecision; this.refreshMessageStoreAtLogon = refreshMessageStoreAtLogon; this.dataDictionaryProvider = dataDictionaryProvider; this.messageFactory = messageFactory; @@ -579,14 +595,24 @@ public static boolean sendToTarget(Message message) throws SessionNotFound { */ public static boolean sendToTarget(Message message, String qualifier) throws SessionNotFound { try { - final String senderCompID = message.getHeader().getString(SenderCompID.FIELD); - final String targetCompID = message.getHeader().getString(TargetCompID.FIELD); + final String senderCompID = getSenderCompIDFromMessage(message); + final String targetCompID = getTargetCompIDFromMessage(message); return sendToTarget(message, senderCompID, targetCompID, qualifier); } catch (final FieldNotFound e) { throw new SessionNotFound("missing sender or target company ID"); } } + private static String getTargetCompIDFromMessage(final Message message) throws FieldNotFound { + final String targetCompID = message.getHeader().getString(TargetCompID.FIELD); + return targetCompID; + } + + private static String getSenderCompIDFromMessage(final Message message) throws FieldNotFound { + final String senderCompID = message.getHeader().getString(SenderCompID.FIELD); + return senderCompID; + } + /** * Send a message to the session specified by the provided target company * ID. The sender company ID is provided as an argument rather than from the @@ -650,15 +676,23 @@ static void registerSession(Session session) { sessions.put(session.getSessionID(), session); } - static void unregisterSessions(List sessionIds) { + static void unregisterSessions(List sessionIds, boolean doClose) { for (final SessionID sessionId : sessionIds) { - final Session session = sessions.remove(sessionId); - if (session != null) { - try { + unregisterSession(sessionId, doClose); + } + } + + static void unregisterSession(SessionID sessionId, boolean doClose) { + final Session session = sessions.get(sessionId); + if (session != null) { + try { + if (doClose) { session.close(); - } catch (final IOException e) { - log.error("Failed to close session resources", e); } + } catch (final IOException e) { + LOG.error("Failed to close session resources", e); + } finally { + sessions.remove(sessionId); } } } @@ -705,12 +739,15 @@ private void optionallySetID(Header header, int field, String value) { } private void insertSendingTime(Message.Header header) { - header.setUtcTimeStamp(SendingTime.FIELD, SystemTime.getDate(), includeMillis()); + header.setUtcTimeStamp(SendingTime.FIELD, SystemTime.getLocalDateTime(), getTimestampPrecision()); } - private boolean includeMillis() { - return millisecondsInTimeStamp - && sessionID.getBeginString().compareTo(FixVersions.BEGINSTRING_FIX42) >= 0; + private UtcTimestampPrecision getTimestampPrecision() { + if (sessionID.getBeginString().compareTo(FixVersions.BEGINSTRING_FIX42) >= 0) { + return timestampPrecision; + } else { + return UtcTimestampPrecision.SECONDS; + } } /** @@ -900,7 +937,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi // QFJ-650 if (!header.isSetField(MsgSeqNum.FIELD)) { generateLogout("Received message without MsgSeqNum"); - disconnect("Received message without MsgSeqNum: " + message, true); + disconnect("Received message without MsgSeqNum: " + getMessageToLog(message), true); return; } @@ -946,7 +983,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi if (rejectInvalidMessage) { throw e; } else { - getLog().onErrorEvent("Warn: incoming message with " + e + ": " + message); + getLog().onErrorEvent("Warn: incoming message with " + e + ": " + getMessageToLog(message)); } } catch (final FieldException e) { if (message.isSetField(e.getField())) { @@ -955,7 +992,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi } else { getLog().onErrorEvent( "Warn: incoming message with incorrect field: " - + message.getField(e.getField()) + ": " + message); + + message.getField(e.getField()) + ": " + getMessageToLog(message)); } } else { if (rejectInvalidMessage) { @@ -963,47 +1000,54 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi } else { getLog().onErrorEvent( "Warn: incoming message with missing field: " + e.getField() - + ": " + e.getMessage() + ": " + message); + + ": " + e.getMessage() + ": " + getMessageToLog(message)); } } } catch (final FieldNotFound e) { if (rejectInvalidMessage) { throw e; } else { - getLog().onErrorEvent("Warn: incoming " + e + ": " + message); + getLog().onErrorEvent("Warn: incoming " + e + ": " + getMessageToLog(message)); } } } - if (msgType.equals(MsgType.LOGON)) { - nextLogon(message); - } else if (msgType.equals(MsgType.HEARTBEAT)) { - nextHeartBeat(message); - } else if (msgType.equals(MsgType.TEST_REQUEST)) { - nextTestRequest(message); - } else if (msgType.equals(MsgType.SEQUENCE_RESET)) { - nextSequenceReset(message); - } else if (msgType.equals(MsgType.LOGOUT)) { - nextLogout(message); - } else if (msgType.equals(MsgType.RESEND_REQUEST)) { - nextResendRequest(message); - } else if (msgType.equals(MsgType.REJECT)) { - nextReject(message); - } else { - if (!verify(message)) { - return; - } - state.incrNextTargetMsgSeqNum(); + switch (msgType) { + case MsgType.LOGON: + nextLogon(message); + break; + case MsgType.HEARTBEAT: + nextHeartBeat(message); + break; + case MsgType.TEST_REQUEST: + nextTestRequest(message); + break; + case MsgType.SEQUENCE_RESET: + nextSequenceReset(message); + break; + case MsgType.LOGOUT: + nextLogout(message); + break; + case MsgType.RESEND_REQUEST: + nextResendRequest(message); + break; + case MsgType.REJECT: + nextReject(message); + break; + default: + if (!verify(message)) { + return; + } + state.incrNextTargetMsgSeqNum(); + break; } - } catch (final FieldException e) { - getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); - if (resetOrDisconnectIfRequired(message)) { + } catch (final FieldException | IncorrectDataFormat | IncorrectTagValue e) { + if (logErrorAndDisconnectIfRequired(e, message)) { return; } - generateReject(message, e.getSessionRejectReason(), e.getField()); + handleExceptionAndRejectMessage(msgType, message, e); } catch (final FieldNotFound e) { - getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); - if (resetOrDisconnectIfRequired(message)) { + if (logErrorAndDisconnectIfRequired(e, message)) { return; } if (sessionBeginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0 @@ -1018,17 +1062,12 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi generateReject(message, SessionRejectReason.REQUIRED_TAG_MISSING, e.field); } } - } catch (final IncorrectDataFormat e) { - getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); - if (resetOrDisconnectIfRequired(message)) { - return; - } - generateReject(message, SessionRejectReason.INCORRECT_DATA_FORMAT_FOR_VALUE, e.field); - } catch (final IncorrectTagValue e) { - getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); - generateReject(message, SessionRejectReason.VALUE_IS_INCORRECT, e.field); } catch (final InvalidMessage e) { - getLog().onErrorEvent("Skipping invalid message: " + e + ": " + message); + /* InvalidMessage means a low-level error (e.g. checksum problem) and we should + ignore the message and let the problem correct itself (optimistic approach). + Target sequence number is not incremented, so it will trigger a ResendRequest + on the next message that is received. */ + getLog().onErrorEvent("Skipping invalid message: " + e + ": " + getMessageToLog(message)); if (resetOrDisconnectIfRequired(message)) { return; } @@ -1042,11 +1081,13 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi generateLogout(e.getMessage()); } } - state.incrNextTargetMsgSeqNum(); + // Only increment seqnum if we are at the expected seqnum + if (getExpectedTargetNum() == header.getInt(MsgSeqNum.FIELD)) { + state.incrNextTargetMsgSeqNum(); + } disconnect("Logon rejected: " + e, true); } catch (final UnsupportedMessageType e) { - getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); - if (resetOrDisconnectIfRequired(message)) { + if (logErrorAndDisconnectIfRequired(e, message)) { return; } if (sessionBeginString.compareTo(FixVersions.BEGINSTRING_FIX42) >= 0) { @@ -1055,8 +1096,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi generateReject(message, "Unsupported message type"); } } catch (final UnsupportedVersion e) { - getLog().onErrorEvent("Rejecting invalid message: " + e + ": " + message); - if (resetOrDisconnectIfRequired(message)) { + if (logErrorAndDisconnectIfRequired(e, message)) { return; } if (msgType.equals(MsgType.LOGOUT)) { @@ -1070,7 +1110,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi disconnect("Incorrect BeginString: " + e, true); } } catch (final IOException e) { - LogUtil.logThrowable(sessionID, "Error processing message: " + message, e); + LogUtil.logThrowable(sessionID, "Error processing message: " + getMessageToLog(message), e); if (resetOrDisconnectIfRequired(message)) { return; } @@ -1078,7 +1118,7 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi // If there are any other Throwables we might catch them here if desired. // They were most probably thrown out of fromCallback(). if (rejectMessageOnUnhandledException) { - getLog().onErrorEvent("Rejecting message: " + t + ": " + message); + getLog().onErrorEvent("Rejecting message: " + t + ": " + getMessageToLog(message)); if (resetOrDisconnectIfRequired(message)) { return; } @@ -1111,6 +1151,30 @@ private void next(Message message, boolean isProcessingQueuedMessages) throws Fi } } + 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)); + generateReject(message, e.getMessage(), e.getSessionRejectReason(), e.getField()); + } + } + + private void logoutWithErrorMessage(final String reason) throws IOException { + final String errorMessage = "Invalid Logon message: " + (reason != null ? reason : "unspecific reason"); + generateLogout(errorMessage); + state.incrNextTargetMsgSeqNum(); + disconnect(errorMessage, true); + } + + private boolean logErrorAndDisconnectIfRequired(final Exception e, Message message) { + final boolean resetOrDisconnectIfRequired = resetOrDisconnectIfRequired(message); + if (resetOrDisconnectIfRequired) { + getLog().onErrorEvent("Encountered invalid message: " + e + ": " + getMessageToLog(message)); + } + return resetOrDisconnectIfRequired; + } + /** * (Internal use only) */ @@ -1136,7 +1200,7 @@ private boolean resetOrDisconnectIfRequired(Message msg) { getLog().onErrorEvent("Auto reset"); reset(); } catch (final IOException e) { - log.error("Failed reseting: " + e); + LOG.error("Failed resetting: {}", e); } return true; } @@ -1144,7 +1208,7 @@ private boolean resetOrDisconnectIfRequired(Message msg) { try { disconnect("Auto disconnect", false); } catch (final IOException e) { - log.error("Failed disconnecting: " + e); + LOG.error("Failed disconnecting: {}", e); } return true; } @@ -1257,7 +1321,7 @@ private void generateSequenceReset(Message receivedMessage, int beginSeqNo, int header.setBoolean(PossDupFlag.FIELD, true); initializeHeader(header); header.setUtcTimeStamp(OrigSendingTime.FIELD, header.getUtcTimeStamp(SendingTime.FIELD), - includeMillis()); + getTimestampPrecision()); header.setInt(MsgSeqNum.FIELD, beginSeqNo); sequenceReset.setInt(NewSeqNo.FIELD, newSeqNo); sequenceReset.setBoolean(GapFillFlag.FIELD, true); @@ -1267,7 +1331,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 " + receivedMessage); + getLog().onErrorEvent("Received message without MsgSeqNum " + getMessageToLog(receivedMessage)); } } sendRaw(sequenceReset, beginSeqNo); @@ -1289,8 +1353,8 @@ private boolean resendApproved(Message message) throws FieldNotFound { private void initializeResendFields(Message message) throws FieldNotFound { final Message.Header header = message.getHeader(); - final Date sendingTime = header.getUtcTimeStamp(SendingTime.FIELD); - header.setUtcTimeStamp(OrigSendingTime.FIELD, sendingTime, includeMillis()); + final LocalDateTime sendingTime = header.getUtcTimeStamp(SendingTime.FIELD); + header.setUtcTimeStamp(OrigSendingTime.FIELD, sendingTime, getTimestampPrecision()); header.setBoolean(PossDupFlag.FIELD, true); insertSendingTime(header); } @@ -1452,7 +1516,7 @@ 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); + getLog().onErrorEvent("Reject sent for message " + msgSeqNum + ": " + str); } private boolean isPossibleDuplicate(Message message) throws FieldNotFound { @@ -1462,10 +1526,20 @@ private boolean isPossibleDuplicate(Message message) throws FieldNotFound { private void generateReject(Message message, int err, int field) throws IOException, FieldNotFound { - final String reason = SessionRejectReasonText.getMessage(err); + generateReject(message, null, err, field); + } + + private void generateReject(Message message, String text, int err, int field) throws IOException, + FieldNotFound { + final String reason; + if (text != null) { + reason = text; + } else { + reason = SessionRejectReasonText.getMessage(err); + } if (!state.isLogonReceived()) { final String errorMessage = "Tried to send a reject while not logged on: " + reason - + " (field " + field + ")"; + + (reason.endsWith("" + field) ? "" : " (field " + field + ")"); throw new SessionException(errorMessage); } @@ -1522,16 +1596,15 @@ private void generateReject(Message message, int err, int field) throws IOExcept } finally { state.unlockTargetMsgSeqNum(); } - + 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( - "Reject sent for Message " + msgSeqNum + ": " + reason + ":" + field); + getLog().onErrorEvent(logMessage + ": " + reason + (reason.endsWith("" + field) ? "" : ":" + field)); } else if (reason != null) { setRejectReason(reject, reason); - getLog().onErrorEvent("Reject sent for Message " + msgSeqNum + ": " + reason); + getLog().onErrorEvent(logMessage + ": " + reason); } else { - getLog().onErrorEvent("Reject sent for Message " + msgSeqNum); + getLog().onErrorEvent(logMessage); } if (enableLastMsgSeqNumProcessed) { @@ -1559,7 +1632,11 @@ private void setRejectReason(Message reject, int field, String reason, reject.setInt(RefTagID.FIELD, field); reject.setString(Text.FIELD, reason); } else { - reject.setString(Text.FIELD, reason + (includeFieldInReason ? " (" + field + ")" : "")); + String rejectReason = reason; + if (includeFieldInReason && !rejectReason.endsWith("" + field) ) { + rejectReason = rejectReason + ", field=" + field; + } + reject.setString(Text.FIELD, rejectReason); } } @@ -1581,7 +1658,7 @@ 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) : "") + "Reject sent for message " + msgSeqNum + (reason != null ? (": " + reason) : "") + (field != 0 ? (": tag=" + field) : "")); sendRaw(reject, 0); @@ -1699,7 +1776,6 @@ private boolean verify(Message msg, boolean checkTooHigh, boolean checkTooLow) private boolean doTargetTooLow(Message msg) throws FieldNotFound, IOException { if (!isPossibleDuplicate(msg)) { final int msgSeqNum = msg.getHeader().getInt(MsgSeqNum.FIELD); - final String text = "MsgSeqNum too low, expecting " + getExpectedTargetNum() + " but received " + msgSeqNum; generateLogout(text); @@ -1709,14 +1785,22 @@ private boolean doTargetTooLow(Message msg) throws FieldNotFound, IOException { } private void doBadCompID(Message msg) throws IOException, FieldNotFound { - generateReject(msg, SessionRejectReason.COMPID_PROBLEM, 0); - generateLogout(); + if (!MsgType.LOGON.equals(msg.getHeader().getString(MsgType.FIELD))) { + generateReject(msg, BAD_COMPID_REJ_REASON, 0); + generateLogout(BAD_COMPID_TEXT); + } else { + logoutWithErrorMessage(BAD_COMPID_TEXT); + } } private void doBadTime(Message msg) throws IOException, FieldNotFound { try { - generateReject(msg, SessionRejectReason.SENDINGTIME_ACCURACY_PROBLEM, 0); - generateLogout(); + if (!MsgType.LOGON.equals(msg.getHeader().getString(MsgType.FIELD))) { + generateReject(msg, BAD_TIME_REJ_REASON, SendingTime.FIELD); + generateLogout(BAD_TIME_TEXT); + } else { + logoutWithErrorMessage(BAD_TIME_TEXT); + } } catch (final SessionException ex) { generateLogout(ex.getMessage()); throw ex; @@ -1727,8 +1811,8 @@ private boolean isGoodTime(Message message) throws FieldNotFound { if (!checkLatency) { return true; } - final Date sendingTime = message.getHeader().getUtcTimeStamp(SendingTime.FIELD); - return Math.abs(SystemTime.currentTimeMillis() - sendingTime.getTime()) / 1000 <= maxLatency; + final LocalDateTime sendingTime = message.getHeader().getUtcTimeStamp(SendingTime.FIELD); + return Math.abs(SystemTime.currentTimeMillis() - sendingTime.toInstant(ZoneOffset.UTC).toEpochMilli()) / 1000 <= maxLatency; } private void fromCallback(String msgType, Message msg, SessionID sessionID2) @@ -1790,6 +1874,7 @@ public void next() throws IOException { } return; // since we are outside of session time window } else { + // reset when session becomes active resetIfSessionNotCurrent(sessionID, now); } } @@ -1809,6 +1894,8 @@ public void next() throws IOException { return; } } + // QFJ-926 - reset session before initiating Logon + resetIfSessionNotCurrent(sessionID, SystemTime.currentTimeMillis()); if (generateLogon()) { getLog().onEvent("Initiated logon request"); } else { @@ -1834,7 +1921,7 @@ public void next() throws IOException { disconnect("Timed out waiting for heartbeat", true); stateListener.onHeartBeatTimeout(); } else { - log.warn("Heartbeat failure detected but deactivated"); + LOG.warn("Heartbeat failure detected but deactivated"); } } else { if (state.isTestRequestNeeded()) { @@ -1912,25 +1999,23 @@ private boolean generateLogon() throws IOException { return sendRaw(logon, 0); } - /** - * Use disconnect(reason, logError) instead. - * - * @deprecated - */ - @Deprecated - public void disconnect() throws IOException { - disconnect("Other reason", true); - } - /** * Logs out from session and closes the network connection. - * + * + * This method should not be called from user-code since it is likely + * to deadlock when called from a different thread than the Session thread + * and messages are sent/received concurrently. + * Instead the logout() method should be used where possible. + * * @param reason the reason why the session is disconnected * @param logError set to true if this disconnection is an error * @throws IOException IO error */ public void disconnect(String reason, boolean logError) throws IOException { try { + final boolean logonReceived = state.isLogonReceived(); + final boolean logonSent = state.isLogonSent(); + synchronized (responderLock) { if (!hasResponder()) { if (!ENCOUNTERED_END_OF_STREAM.equals(reason)) { @@ -1942,14 +2027,12 @@ public void disconnect(String reason, boolean logError) throws IOException { if (logError) { getLog().onErrorEvent(msg); } else { - log.info("[" + getSessionID() + "] " + msg); + getLog().onEvent(msg); } responder.disconnect(); setResponder(null); } - final boolean logonReceived = state.isLogonReceived(); - final boolean logonSent = state.isLogonSent(); if (logonReceived || logonSent) { try { application.onLogout(sessionID); @@ -1959,11 +2042,12 @@ public void disconnect(String reason, boolean logError) throws IOException { stateListener.onLogout(); } + } finally { // QFJ-457 now enabled again if acceptor if (!state.isInitiator()) { setEnabled(true); } - } finally { + state.setLogonReceived(false); state.setLogonSent(false); state.setLogoutSent(false); @@ -1990,6 +2074,9 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre throw new RejectLogon("Logon attempt not within session time"); } + // QFJ-926 - reset session before accepting Logon + resetIfSessionNotCurrent(sessionID, SystemTime.currentTimeMillis()); + if (isStateRefreshNeeded(MsgType.LOGON)) { getLog().onEvent("Refreshing message/state store at logon"); getStore().refresh(); @@ -2042,7 +2129,13 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre if (logon.isSetField(NextExpectedMsgSeqNum.FIELD) && enableNextExpectedMsgSeqNum) { final int targetWantsNextSeqNumToBe = logon.getInt(NextExpectedMsgSeqNum.FIELD); - final int actualNextNum = state.getMessageStore().getNextSenderMsgSeqNum(); + state.lockSenderMsgSeqNum(); + final int actualNextNum; + try { + actualNextNum = state.getNextSenderMsgSeqNum(); + } finally { + state.unlockSenderMsgSeqNum(); + } // Is the 789 we received too high ?? if (targetWantsNextSeqNumToBe > actualNextNum) { // barf! we can't resend what we never sent! something unrecoverable has happened. @@ -2143,12 +2236,12 @@ 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(); + 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); + 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); @@ -2197,7 +2290,7 @@ private void resendMessages(Message receivedMessage, int beginSeqNo, int endSeqN if (begin != 0) { generateSequenceReset(receivedMessage, begin, msgSeqNum); } - getLog().onEvent("Resending Message: " + msgSeqNum); + getLog().onEvent("Resending message: " + msgSeqNum); send(msg.toString()); begin = 0; appMessageJustSent = true; @@ -2351,11 +2444,11 @@ private boolean validatePossDup(Message msg) throws FieldNotFound, IOException { if (!msgType.equals(MsgType.SEQUENCE_RESET)) { if (header.isSetField(OrigSendingTime.FIELD)) { - final Date origSendingTime = header.getUtcTimeStamp(OrigSendingTime.FIELD); - final Date sendingTime = header.getUtcTimeStamp(SendingTime.FIELD); + final LocalDateTime origSendingTime = header.getUtcTimeStamp(OrigSendingTime.FIELD); + final LocalDateTime sendingTime = header.getUtcTimeStamp(SendingTime.FIELD); if (origSendingTime.compareTo(sendingTime) > 0) { - generateReject(msg, SessionRejectReason.SENDINGTIME_ACCURACY_PROBLEM, 0); - generateLogout(); + generateReject(msg, BAD_TIME_REJ_REASON, OrigSendingTime.FIELD); + generateLogout(BAD_ORIG_TIME_TEXT); return false; } } else { @@ -2491,7 +2584,7 @@ private boolean sendRaw(Message message, int num) { return result; } catch (final IOException e) { - logThrowable(getLog(), "Error Reading/Writing in MessageStore", e); + logThrowable(getLog(), "Error reading/writing in MessageStore", e); return false; } catch (final FieldNotFound e) { logThrowable(state.getLog(), "Error accessing message fields", e); @@ -2507,8 +2600,15 @@ private void enqueueMessage(final Message msg, final int msgSeqNum) { } private void resetState() { - state.reset(); - stateListener.onReset(); + if (!isResettingState.compareAndSet(false, true)) { + return; + } + try { + state.reset(); + stateListener.onReset(); + } finally { + isResettingState.set(false); + } } /** @@ -2548,24 +2648,12 @@ private boolean isCorrectCompID(Message message) throws FieldNotFound { if (!checkCompID) { return true; } - final String senderCompID = message.getHeader().getString(SenderCompID.FIELD); - final String targetCompID = message.getHeader().getString(TargetCompID.FIELD); + final String senderCompID = getSenderCompIDFromMessage(message); + final String targetCompID = getTargetCompIDFromMessage(message); return sessionID.getSenderCompID().equals(targetCompID) && sessionID.getTargetCompID().equals(senderCompID); } - /** - * Set the data dictionary. (QF Compatibility) - * - * @deprecated - * @param dataDictionary - */ - @Deprecated - public void setDataDictionary(DataDictionary dataDictionary) { - throw new UnsupportedOperationException( - "Modification of session dictionary is not supported in QFJ"); - } - public DataDictionary getDataDictionary() { if (!sessionID.isFIXT()) { // For pre-FIXT sessions, the session data dictionary is the same as the application @@ -2758,18 +2846,18 @@ public void setTargetDefaultApplicationVersionID(ApplVerID applVerID) { } private static String extractNumber(String txt, int from) { - String ret = ""; + final StringBuilder ret = new StringBuilder(txt.length() - from); for (int i = from; i != txt.length(); ++i) { final char c = txt.charAt(i); if (c >= '0' && c <= '9') { - ret += c; + ret.append(c); } else { if (ret.length() != 0) { break; } } } - return ret.trim(); + return ret.toString(); } protected static Integer extractExpectedSequenceNumber(String txt) { @@ -2831,13 +2919,15 @@ public boolean isAllowedForSession(InetAddress remoteInetAddress) { } /** - * Closes session resources. This is for internal use and should typically - * not be called by an user application. + * Closes session resources and unregisters session. This is for internal + * use and should typically not be called by an user application. */ @Override public void close() throws IOException { closeIfCloseable(getLog()); closeIfCloseable(getStore()); + // clean up session just in case close() was not called from Session.unregisterSession() + unregisterSession(this.sessionID, false); } private void closeIfCloseable(Object resource) throws IOException { @@ -2853,4 +2943,8 @@ private void resetIfSessionNotCurrent(SessionID sessionID, long time) throws IOE } } + private String getMessageToLog(final Message message) { + return (message.toRawString() != null ? message.toRawString() : message.toString()); + } + } diff --git a/quickfixj-core/src/main/java/quickfix/SessionFactory.java b/quickfixj-core/src/main/java/quickfix/SessionFactory.java index 98d6e8ec5..234f26836 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionFactory.java +++ b/quickfixj-core/src/main/java/quickfix/SessionFactory.java @@ -28,17 +28,17 @@ public interface SessionFactory { * Specifies the connection type for a session. Valid values are "initiator" * and "acceptor". */ - public static final String SETTING_CONNECTION_TYPE = "ConnectionType"; + String SETTING_CONNECTION_TYPE = "ConnectionType"; /** * Instructs the connection-related code to continue if there is an error * creating or initializing a session. In other words, one bad session won't * stop the initialization of other sessions. */ - public static final String SETTING_CONTINUE_INIT_ON_ERROR = "ContinueInitializationOnError"; + String SETTING_CONTINUE_INIT_ON_ERROR = "ContinueInitializationOnError"; - public static final String ACCEPTOR_CONNECTION_TYPE = "acceptor"; - public static final String INITIATOR_CONNECTION_TYPE = "initiator"; + String ACCEPTOR_CONNECTION_TYPE = "acceptor"; + String INITIATOR_CONNECTION_TYPE = "initiator"; Session create(SessionID sessionID, SessionSettings settings) throws ConfigError; diff --git a/quickfixj-core/src/main/java/quickfix/SessionRejectReasonText.java b/quickfixj-core/src/main/java/quickfix/SessionRejectReasonText.java index f790f6b28..39cbe1bf4 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionRejectReasonText.java +++ b/quickfixj-core/src/main/java/quickfix/SessionRejectReasonText.java @@ -19,12 +19,12 @@ package quickfix; -import java.util.HashMap; - import quickfix.field.SessionRejectReason; +import java.util.HashMap; + class SessionRejectReasonText extends SessionRejectReason { - private static final HashMap rejectReasonText = new HashMap(); + private static final HashMap rejectReasonText = new HashMap<>(); static { rejectReasonText.put(INVALID_TAG_NUMBER, "Invalid tag number"); diff --git a/quickfixj-core/src/main/java/quickfix/SessionSchedule.java b/quickfixj-core/src/main/java/quickfix/SessionSchedule.java index 269640c4c..a19d612da 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionSchedule.java +++ b/quickfixj-core/src/main/java/quickfix/SessionSchedule.java @@ -19,337 +19,28 @@ package quickfix; -import java.text.SimpleDateFormat; import java.util.Calendar; -import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** - * Corresponds to SessionTime in C++ code + * Used to decide when to login and out of FIX sessions */ -public class SessionSchedule { - 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 nonStopSession; - protected final static Logger log = LoggerFactory.getLogger(SessionSchedule.class); - - public SessionSchedule(SessionSettings settings, SessionID sessionID) throws ConfigError, - FieldConvertError { - - nonStopSession = settings.isSetting(sessionID, Session.SETTING_NON_STOP_SESSION) && settings.getBool(sessionID, Session.SETTING_NON_STOP_SESSION); - TimeZone defaultTimeZone = getDefaultTimeZone(settings, sessionID); - if (nonStopSession) { - startTime = endTime = new TimeEndPoint(NOT_SET, 0, 0, 0, defaultTimeZone); - return; - } - - boolean startDayPresent = settings.isSetting(sessionID, Session.SETTING_START_DAY); - boolean endDayPresent = settings.isSetting(sessionID, Session.SETTING_END_DAY); - - if (startDayPresent && !endDayPresent) { - throw new ConfigError("Session " + sessionID + ": StartDay used without EndDay"); - } - - if (endDayPresent && !startDayPresent) { - throw new ConfigError("Session " + sessionID + ": EndDay used without StartDay"); - } - - 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()); - } - - private TimeEndPoint getTimeEndPoint(SessionSettings settings, SessionID sessionID, - TimeZone defaultTimeZone, String timeSetting, String daySetting) throws ConfigError, - FieldConvertError { - - Matcher matcher = TIME_PATTERN.matcher(settings.getString(sessionID, timeSetting)); - if (!matcher.find()) { - throw new ConfigError("Session " + sessionID + ": could not parse time '" - + settings.getString(sessionID, timeSetting) + "'."); - } - - return new TimeEndPoint(getDay(settings, sessionID, daySetting, NOT_SET), - 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 { - TimeZone sessionTimeZone; - if (settings.isSetting(sessionID, Session.SETTING_TIMEZONE)) { - String sessionTimeZoneID = settings.getString(sessionID, Session.SETTING_TIMEZONE); - sessionTimeZone = TimeZone.getTimeZone(sessionTimeZoneID); - if ("GMT".equals(sessionTimeZone.getID()) && !"GMT".equals(sessionTimeZoneID)) { - throw new ConfigError("Unrecognized time zone '" + sessionTimeZoneID - + "' for session " + sessionID); - } - } else { - sessionTimeZone = TimeZone.getTimeZone("UTC"); - } - return sessionTimeZone; - } - - private TimeZone getTimeZone(String tz, TimeZone defaultZone) { - return "".equals(tz) ? defaultZone : TimeZone.getTimeZone(tz.trim()); - } - - private class TimeEndPoint { - private final int weekDay; - private final int hour; - private final int minute; - private final int second; - private final int timeInSeconds; - private final TimeZone tz; - - public TimeEndPoint(int day, int hour, int minute, int second, TimeZone tz) { - weekDay = day; - this.hour = hour; - this.minute = minute; - this.second = second; - this.tz = tz; - timeInSeconds = timeInSeconds(hour, minute, second); - } - - public int getHour() { - return hour; - } - - public int getMinute() { - return minute; - } - - public int getSecond() { - return second; - } - - public String toString() { - try { - Calendar calendar = Calendar.getInstance(tz); - calendar.set(Calendar.HOUR_OF_DAY, hour); - calendar.set(Calendar.MINUTE, minute); - calendar.set(Calendar.SECOND, second); - final SimpleDateFormat utc = new SimpleDateFormat("HH:mm:ss"); - utc.setTimeZone(TimeZone.getTimeZone("UTC")); - return (isSet(weekDay) ? DayConverter.toString(weekDay) + "," : "") - + utc.format(calendar.getTime()) + "-" + utc.getTimeZone().getID(); - } catch (ConfigError e) { - return "ERROR: " + e; - } - } - - public int getDay() { - return weekDay; - } - - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o instanceof TimeEndPoint) { - TimeEndPoint otherTime = (TimeEndPoint) o; - return timeInSeconds == otherTime.timeInSeconds; - } - return false; - } - - public int hashCode() { - assert false : "hashCode not supported"; - return 0; - } - - public TimeZone getTimeZone() { - return tz; - } - } - - private TimeInterval theMostRecentIntervalBefore(Calendar t) { - TimeInterval timeInterval = new TimeInterval(); - Calendar intervalStart = timeInterval.getStart(); - intervalStart.setTimeZone(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.MILLISECOND, 0); - - Calendar intervalEnd = timeInterval.getEnd(); - intervalEnd.setTimeZone(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.MILLISECOND, 0); - - if (isSet(startTime.getDay())) { - intervalStart.set(Calendar.DAY_OF_WEEK, startTime.getDay()); - if (intervalStart.getTimeInMillis() > t.getTimeInMillis()) { - intervalStart.add(Calendar.WEEK_OF_YEAR, -1); - intervalEnd.add(Calendar.WEEK_OF_YEAR, -1); - } - } else if (intervalStart.getTimeInMillis() > t.getTimeInMillis()) { - intervalStart.add(Calendar.DAY_OF_YEAR, -1); - intervalEnd.add(Calendar.DAY_OF_YEAR, -1); - } - - if (isSet(endTime.getDay())) { - intervalEnd.set(Calendar.DAY_OF_WEEK, endTime.getDay()); - if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) { - intervalEnd.add(Calendar.WEEK_OF_MONTH, 1); - } - } else if (intervalEnd.getTimeInMillis() <= intervalStart.getTimeInMillis()) { - intervalEnd.add(Calendar.DAY_OF_WEEK, 1); - } - - return timeInterval; - } - - private static class TimeInterval { - private final Calendar start = SystemTime.getUtcCalendar(); - private final Calendar end = SystemTime.getUtcCalendar(); - - public boolean isContainingTime(Calendar t) { - return t.compareTo(start) >= 0 && t.compareTo(end) <= 0; - } - - public String toString() { - return start.getTime() + " --> " + end.getTime(); - } - - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof TimeInterval)) { - return false; - } - TimeInterval otherInterval = (TimeInterval) other; - return start.equals(otherInterval.start) && end.equals(otherInterval.end); - } - - public int hashCode() { - assert false : "hashCode not supported"; - return 0; - } - - public Calendar getStart() { - return start; - } - - public Calendar getEnd() { - return end; - } - } - - public boolean isSameSession(Calendar time1, Calendar time2) { - if (nonStopSession) - return true; - TimeInterval interval1 = theMostRecentIntervalBefore(time1); - if (!interval1.isContainingTime(time1)) { - return false; - } - TimeInterval interval2 = theMostRecentIntervalBefore(time2); - return interval2.isContainingTime(time2) && interval1.equals(interval2); - } - - public boolean isNonStopSession() { - return nonStopSession; - } - - private boolean isDailySession() { - return !isSet(startTime.getDay()) && !isSet(endTime.getDay()); - } - - public boolean isSessionTime() { - if(nonStopSession) { - return true; - } - Calendar now = SystemTime.getUtcCalendar(); - TimeInterval interval = theMostRecentIntervalBefore(now); - return interval.isContainingTime(now); - } - - public String toString() { - StringBuilder buf = new StringBuilder(); - - SimpleDateFormat dowFormat = new SimpleDateFormat("EEEE"); - dowFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - - SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss-z"); - timeFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - - TimeInterval ti = theMostRecentIntervalBefore(SystemTime.getUtcCalendar()); - - formatTimeInterval(buf, ti, timeFormat, false); - - // Now the localized equivalents, if necessary - if (!startTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE) - || !endTime.getTimeZone().equals(SystemTime.UTC_TIMEZONE)) { - buf.append(" ("); - formatTimeInterval(buf, ti, timeFormat, true); - buf.append(")"); - } - - return buf.toString(); - } - - private void formatTimeInterval(StringBuilder buf, TimeInterval timeInterval, - SimpleDateFormat timeFormat, boolean local) { - if (!isDailySession()) { - buf.append("weekly, "); - formatDayOfWeek(buf, startTime.getDay()); - buf.append(" "); - } else { - buf.append("daily, "); - } - - if (local) { - timeFormat.setTimeZone(startTime.getTimeZone()); - } - buf.append(timeFormat.format(timeInterval.getStart().getTime())); - - buf.append(" - "); - - if (!isDailySession()) { - formatDayOfWeek(buf, endTime.getDay()); - buf.append(" "); - } - if (local) { - timeFormat.setTimeZone(endTime.getTimeZone()); - } - buf.append(timeFormat.format(timeInterval.getEnd().getTime())); - } +public interface SessionSchedule { - private void formatDayOfWeek(StringBuilder buf, int dayOfWeek) { - try { - String dayName = DayConverter.toString(dayOfWeek).toUpperCase(); - if (dayName.length() > 3) { - dayName = dayName.substring(0, 3); - } - buf.append(dayName); - } catch (ConfigError e) { - buf.append("[Error: unknown day ").append(dayOfWeek).append("]"); - } - } + /** + * Predicate for determining if the two times are in the same session + * @param time1 test time 1 + * @param time2 test time 2 + * @return return true if in the same session + */ + boolean isSameSession(Calendar time1, Calendar time2); - private int getDay(SessionSettings settings, SessionID sessionID, String key, int defaultValue) - throws ConfigError, FieldConvertError { - return settings.isSetting(sessionID, key) ? - DayConverter.toInteger(settings.getString(sessionID, key)) - : NOT_SET; - } + boolean isNonStopSession(); - private boolean isSet(int value) { - return value != NOT_SET; - } + /** + * Predicate for determining if the session should be active at the current time. + * + * @return true if session should be active, false otherwise. + */ + boolean isSessionTime(); - private int timeInSeconds(int hour, int minute, int second) { - return (hour * 3600) + (minute * 60) + second; - } } diff --git a/quickfixj-core/src/main/java/quickfix/SessionScheduleFactory.java b/quickfixj-core/src/main/java/quickfix/SessionScheduleFactory.java new file mode 100644 index 000000000..e4a83547a --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/SessionScheduleFactory.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * 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; + +/** + * Creates a SessionSchedule based on the specified settings. + */ +public interface SessionScheduleFactory { + SessionSchedule create(SessionID sessionID, SessionSettings settings) throws ConfigError; +} diff --git a/quickfixj-core/src/main/java/quickfix/SessionSettings.java b/quickfixj-core/src/main/java/quickfix/SessionSettings.java index 0b68e3f66..6d9788e28 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionSettings.java +++ b/quickfixj-core/src/main/java/quickfix/SessionSettings.java @@ -19,6 +19,10 @@ package quickfix; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quickfix.field.converter.BooleanConverter; + import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -42,11 +46,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import quickfix.field.converter.BooleanConverter; - /** * Settings for sessions. Settings are grouped by FIX version and target company * ID. There is also a default settings section that is inherited by the @@ -263,12 +262,7 @@ public int getInt(SessionID sessionID, String key) throws ConfigError, FieldConv } private Properties getOrCreateSessionProperties(SessionID sessionID) { - Properties p = sections.get(sessionID); - if (p == null) { - p = new Properties(sections.get(DEFAULT_SESSION_ID)); - sections.put(sessionID, p); - } - return p; + return sections.computeIfAbsent(sessionID, k -> new Properties(sections.get(DEFAULT_SESSION_ID))); } /** @@ -373,10 +367,10 @@ public void setBool(SessionID sessionID, String key, boolean value) { getOrCreateSessionProperties(sessionID).setProperty(key, BooleanConverter.convert(value)); } - private final HashMap sections = new HashMap(); + private final HashMap sections = new HashMap<>(); public Iterator sectionIterator() { - final HashSet nondefaultSessions = new HashSet(sections.keySet()); + final HashSet nondefaultSessions = new HashSet<>(sections.keySet()); nondefaultSessions.remove(DEFAULT_SESSION_ID); return nondefaultSessions.iterator(); } @@ -729,7 +723,7 @@ public static int[] parseSettingReconnectInterval(String raw) { } final String multiplierCharacter = raw.contains("*") ? "\\*" : "x"; final String[] data = raw.split(";"); - final List result = new ArrayList(); + final List result = new ArrayList<>(); for (final String multi : data) { final String[] timesSec = multi.split(multiplierCharacter); int times; @@ -767,7 +761,7 @@ public static Set parseRemoteAddresses(String raw) { return null; } final String[] data = raw.split(","); - final Set result = new HashSet(); + final Set result = new HashSet<>(); for (final String multi : data) { try { result.add(InetAddress.getByName(multi)); diff --git a/quickfixj-core/src/main/java/quickfix/SessionState.java b/quickfixj-core/src/main/java/quickfix/SessionState.java index c0f11fae7..ef666c852 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionState.java +++ b/quickfixj-core/src/main/java/quickfix/SessionState.java @@ -72,7 +72,7 @@ public final class SessionState { private final AtomicInteger nextExpectedMsgSeqNum = new AtomicInteger(0); // The messageQueue should be accessed from a single thread - private final Map messageQueue = new LinkedHashMap(); + private final Map messageQueue = new LinkedHashMap<>(); public SessionState(Object lock, Log log, int heartBeatInterval, boolean initiator, MessageStore messageStore, double testRequestDelayMultiplier) { diff --git a/quickfixj-core/src/main/java/quickfix/SleepycatStore.java b/quickfixj-core/src/main/java/quickfix/SleepycatStore.java index 34966a4cf..e9e7aae1c 100644 --- a/quickfixj-core/src/main/java/quickfix/SleepycatStore.java +++ b/quickfixj-core/src/main/java/quickfix/SleepycatStore.java @@ -221,16 +221,14 @@ public synchronized void get(int startSequence, int endSequence, Collection" - + new String(messageBytes.getData(), charsetEncoding) + " for search key/data: " - + sequenceKey + "=>" + messageBytes); + 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); diff --git a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java index bda6ae8ee..b64f807d5 100644 --- a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java @@ -28,10 +28,37 @@ * sessions. */ public class SocketAcceptor extends AbstractSocketAcceptor { - private Boolean isStarted = Boolean.FALSE; - private final Object lock = new Object(); + private volatile Boolean isStarted = Boolean.FALSE; private final SingleThreadedEventHandlingStrategy eventHandlingStrategy; + private SocketAcceptor(Builder builder) throws ConfigError { + super(builder.application, builder.messageStoreFactory, builder.settings, + builder.logFactory, builder.messageFactory); + + if (builder.queueCapacity >= 0) { + eventHandlingStrategy + = new SingleThreadedEventHandlingStrategy(this, builder.queueCapacity); + } else { + eventHandlingStrategy + = new SingleThreadedEventHandlingStrategy(this, builder.queueLowerWatermark, builder.queueUpperWatermark); + } + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder extends AbstractSessionConnectorBuilder { + private Builder() { + super(Builder.class); + } + + @Override + protected SocketAcceptor doBuild() throws ConfigError { + return new SocketAcceptor(this); + } + } + public SocketAcceptor(Application application, MessageStoreFactory messageStoreFactory, SessionSettings settings, LogFactory logFactory, MessageFactory messageFactory, int queueCapacity) @@ -70,30 +97,23 @@ public SocketAcceptor(SessionFactory sessionFactory, SessionSettings settings) t eventHandlingStrategy = new SingleThreadedEventHandlingStrategy(this, DEFAULT_QUEUE_CAPACITY); } - @Override - public void block() throws ConfigError, RuntimeError { - initialize(false); - } - @Override public void start() throws ConfigError, RuntimeError { initialize(true); } private void initialize(boolean blockInThread) throws ConfigError { - synchronized (lock) { - if (isStarted.equals(Boolean.FALSE)) { - startAcceptingConnections(); - if (blockInThread) { - eventHandlingStrategy.blockInThread(); - isStarted = Boolean.TRUE; - } else { - isStarted = Boolean.TRUE; - eventHandlingStrategy.block(); - } + if (isStarted.equals(Boolean.FALSE)) { + eventHandlingStrategy.setExecutor(longLivedExecutor); + startAcceptingConnections(); + isStarted = Boolean.TRUE; + if (blockInThread) { + eventHandlingStrategy.blockInThread(); } else { - log.warn("Ignored attempt to start already running SocketAcceptor."); + eventHandlingStrategy.block(); } + } else { + log.warn("Ignored attempt to start already running SocketAcceptor."); } } @@ -104,18 +124,19 @@ public void stop() { @Override public void stop(boolean forceDisconnect) { - eventHandlingStrategy.stopHandlingMessages(); - synchronized (lock) { + if (isStarted.equals(Boolean.TRUE)) { try { try { + logoutAllSessions(forceDisconnect); stopAcceptingConnections(); } catch (ConfigError e) { log.error("Error when stopping acceptor.", e); } - logoutAllSessions(forceDisconnect); stopSessionTimer(); } finally { - Session.unregisterSessions(getSessions()); + eventHandlingStrategy.stopHandlingMessages(); + Session.unregisterSessions(getSessions(), true); + clearConnectorSessions(); isStarted = Boolean.FALSE; } } diff --git a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java index df10417ba..efad4f404 100644 --- a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java @@ -28,10 +28,37 @@ * sessions. */ public class SocketInitiator extends AbstractSocketInitiator { - private Boolean isStarted = Boolean.FALSE; - private final Object lock = new Object(); + private volatile Boolean isStarted = Boolean.FALSE; private final SingleThreadedEventHandlingStrategy eventHandlingStrategy; + private SocketInitiator(Builder builder) throws ConfigError { + super(builder.application, builder.messageStoreFactory, builder.settings, + builder.logFactory, builder.messageFactory); + + if (builder.queueCapacity >= 0) { + eventHandlingStrategy + = new SingleThreadedEventHandlingStrategy(this, builder.queueCapacity); + } else { + eventHandlingStrategy + = new SingleThreadedEventHandlingStrategy(this, builder.queueLowerWatermark, builder.queueUpperWatermark); + } + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder extends AbstractSessionConnectorBuilder { + private Builder() { + super(Builder.class); + } + + @Override + protected SocketInitiator doBuild() throws ConfigError { + return new SocketInitiator(this); + } + } + public SocketInitiator(Application application, MessageStoreFactory messageStoreFactory, SessionSettings settings, MessageFactory messageFactory, int queueCapacity) throws ConfigError { super(application, messageStoreFactory, settings, new ScreenLogFactory(settings), @@ -79,11 +106,6 @@ public SocketInitiator(SessionFactory sessionFactory, SessionSettings settings, eventHandlingStrategy = new SingleThreadedEventHandlingStrategy(this, queueCapacity); } - @Override - public void block() throws ConfigError, RuntimeError { - initialize(false); - } - @Override public void start() throws ConfigError, RuntimeError { initialize(true); @@ -96,36 +118,35 @@ public void stop() { @Override public void stop(boolean forceDisconnect) { - eventHandlingStrategy.stopHandlingMessages(); - synchronized (lock) { + if (isStarted.equals(Boolean.TRUE)) { try { logoutAllSessions(forceDisconnect); stopInitiators(); } finally { - Session.unregisterSessions(getSessions()); + eventHandlingStrategy.stopHandlingMessages(); + Session.unregisterSessions(getSessions(), true); + clearConnectorSessions(); isStarted = Boolean.FALSE; } } } private void initialize(boolean blockInThread) throws ConfigError { - synchronized (lock) { - if (isStarted.equals(Boolean.FALSE)) { - createSessionInitiators(); - for (Session session : getSessionMap().values()) { - Session.registerSession(session); - } - startInitiators(); - if (blockInThread) { - eventHandlingStrategy.blockInThread(); - isStarted = Boolean.TRUE; - } else { - isStarted = Boolean.TRUE; - eventHandlingStrategy.block(); - } + if (isStarted.equals(Boolean.FALSE)) { + eventHandlingStrategy.setExecutor(longLivedExecutor); + createSessionInitiators(); + for (Session session : getSessionMap().values()) { + Session.registerSession(session); + } + startInitiators(); + isStarted = Boolean.TRUE; + if (blockInThread) { + eventHandlingStrategy.blockInThread(); } else { - log.warn("Ignored attempt to start already running SocketInitiator."); + eventHandlingStrategy.block(); } + } else { + log.warn("Ignored attempt to start already running SocketInitiator."); } } diff --git a/quickfixj-core/src/main/java/quickfix/SystemTime.java b/quickfixj-core/src/main/java/quickfix/SystemTime.java index 6687f569c..85d73c47b 100644 --- a/quickfixj-core/src/main/java/quickfix/SystemTime.java +++ b/quickfixj-core/src/main/java/quickfix/SystemTime.java @@ -19,6 +19,8 @@ package quickfix; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; @@ -30,9 +32,15 @@ public class SystemTime { public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC"); private static final SystemTimeSource DEFAULT_TIME_SOURCE = new SystemTimeSource() { + @Override public long getTime() { return System.currentTimeMillis(); } + + @Override + public LocalDateTime getNow() { + return LocalDateTime.now(ZoneOffset.UTC); + } }; private static volatile SystemTimeSource systemTimeSource = DEFAULT_TIME_SOURCE; @@ -40,11 +48,19 @@ public long getTime() { public static long currentTimeMillis() { return systemTimeSource.getTime(); } + + public static LocalDateTime now() { + return systemTimeSource.getNow(); + } public static Date getDate() { return new Date(currentTimeMillis()); } + public static LocalDateTime getLocalDateTime() { + return now(); + } + public static void setTimeSource(SystemTimeSource systemTimeSource) { SystemTime.systemTimeSource = systemTimeSource != null ? systemTimeSource : DEFAULT_TIME_SOURCE; diff --git a/quickfixj-core/src/main/java/quickfix/SystemTimeSource.java b/quickfixj-core/src/main/java/quickfix/SystemTimeSource.java index f2e2b104f..c14af90f8 100644 --- a/quickfixj-core/src/main/java/quickfix/SystemTimeSource.java +++ b/quickfixj-core/src/main/java/quickfix/SystemTimeSource.java @@ -19,6 +19,8 @@ package quickfix; +import java.time.LocalDateTime; + /** * Interface for obtaining system time. A system time source should be used * instead of direct system time to facilitate unit testing. @@ -31,4 +33,11 @@ public interface SystemTimeSource { * @return current (possible simulated) time */ long getTime(); + + /** + * Obtain current LocalDateTime. + * + * @return current (possible simulated) time up to nanosecond precision. + */ + LocalDateTime getNow(); } diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java index f5d65ada8..8f35bb801 100644 --- a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java @@ -29,6 +29,34 @@ public class ThreadedSocketAcceptor extends AbstractSocketAcceptor { private final ThreadPerSessionEventHandlingStrategy eventHandlingStrategy; + private ThreadedSocketAcceptor(Builder builder) throws ConfigError { + super(builder.application, builder.messageStoreFactory, builder.settings, + builder.logFactory, builder.messageFactory); + + if (builder.queueCapacity >= 0) { + eventHandlingStrategy + = new ThreadPerSessionEventHandlingStrategy(this, builder.queueCapacity); + } else { + eventHandlingStrategy + = new ThreadPerSessionEventHandlingStrategy(this, builder.queueLowerWatermark, builder.queueUpperWatermark); + } + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder extends AbstractSessionConnectorBuilder { + private Builder() { + super(Builder.class); + } + + @Override + protected ThreadedSocketAcceptor doBuild() throws ConfigError { + return new ThreadedSocketAcceptor(this); + } + } + public ThreadedSocketAcceptor(Application application, MessageStoreFactory messageStoreFactory, SessionSettings settings, LogFactory logFactory, MessageFactory messageFactory, int queueCapacity ) @@ -70,6 +98,7 @@ public ThreadedSocketAcceptor(SessionFactory sessionFactory, SessionSettings set } public void start() throws ConfigError, RuntimeError { + eventHandlingStrategy.setExecutor(longLivedExecutor); startAcceptingConnections(); } @@ -79,14 +108,15 @@ public void stop() { public void stop(boolean forceDisconnect) { try { + logoutAllSessions(forceDisconnect); stopAcceptingConnections(); } catch (ConfigError e) { log.error("Error when stopping acceptor.", e); } - logoutAllSessions(forceDisconnect); stopSessionTimer(); eventHandlingStrategy.stopDispatcherThreads(); - Session.unregisterSessions(getSessions()); + Session.unregisterSessions(getSessions(), true); + clearConnectorSessions(); } public void block() throws ConfigError, RuntimeError { diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java index 1bdb4ed8a..bf42ce20a 100644 --- a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java @@ -29,6 +29,34 @@ public class ThreadedSocketInitiator extends AbstractSocketInitiator { private final ThreadPerSessionEventHandlingStrategy eventHandlingStrategy; + private ThreadedSocketInitiator(Builder builder) throws ConfigError { + super(builder.application, builder.messageStoreFactory, builder.settings, + builder.logFactory, builder.messageFactory); + + if (builder.queueCapacity >= 0) { + eventHandlingStrategy + = new ThreadPerSessionEventHandlingStrategy(this, builder.queueCapacity); + } else { + eventHandlingStrategy + = new ThreadPerSessionEventHandlingStrategy(this, builder.queueLowerWatermark, builder.queueUpperWatermark); + } + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder extends AbstractSessionConnectorBuilder { + private Builder() { + super(Builder.class); + } + + @Override + protected ThreadedSocketInitiator doBuild() throws ConfigError { + return new ThreadedSocketInitiator(this); + } + } + public ThreadedSocketInitiator(Application application, MessageStoreFactory messageStoreFactory, SessionSettings settings, LogFactory logFactory, MessageFactory messageFactory, int queueCapacity) throws ConfigError { @@ -72,6 +100,7 @@ public ThreadedSocketInitiator(SessionFactory sessionFactory, SessionSettings se } public void start() throws ConfigError, RuntimeError { + eventHandlingStrategy.setExecutor(longLivedExecutor); createSessionInitiators(); startInitiators(); } @@ -81,13 +110,11 @@ public void stop() { } public void stop(boolean forceDisconnect) { - stopInitiators(); logoutAllSessions(forceDisconnect); - if (!forceDisconnect) { - waitForLogout(); - } + stopInitiators(); eventHandlingStrategy.stopDispatcherThreads(); - Session.unregisterSessions(getSessions()); + Session.unregisterSessions(getSessions(), true); + clearConnectorSessions(); } public void block() throws ConfigError, RuntimeError { diff --git a/quickfixj-core/src/main/java/quickfix/UtcDateField.java b/quickfixj-core/src/main/java/quickfix/UtcDateField.java new file mode 100644 index 000000000..353e42463 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/UtcDateField.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * 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.time.LocalDate; + +/** + * A LocalDate-valued message field with up to nanosecond precision. + */ +public class UtcDateField extends Field { + + protected UtcDateField(int field) { + super(field, LocalDate.now()); + } + + protected UtcDateField(int field, LocalDate data) { + super(field, data); + } + + public void setValue(LocalDate value) { + setObject(value); + } + + public LocalDate getValue() { + return getObject(); + } + + public boolean valueEquals(LocalDate value) { + return getValue().equals(value); + } + +} diff --git a/quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java b/quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java index 427b02c26..9926a51fb 100644 --- a/quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java +++ b/quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java @@ -19,17 +19,31 @@ package quickfix; -import java.util.Date; +import java.time.LocalDate; /** * A date-valued message field. */ -public class UtcDateOnlyField extends DateField { +public class UtcDateOnlyField extends Field { + public UtcDateOnlyField(int field) { - super(field); + super(field, LocalDate.now()); } - protected UtcDateOnlyField(int field, Date data) { + protected UtcDateOnlyField(int field, LocalDate data) { super(field, data); } + + public void setValue(LocalDate value) { + setObject(value); + } + + public LocalDate getValue() { + return getObject(); + } + + public boolean valueEquals(LocalDate value) { + return getValue().equals(value); + } + } diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimeField.java b/quickfixj-core/src/main/java/quickfix/UtcTimeField.java new file mode 100644 index 000000000..e913e3e41 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/UtcTimeField.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * 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.time.LocalTime; + +/** + * A LocalTime-valued message field with up to nanosecond precision. + */ +public class UtcTimeField extends Field { + + protected UtcTimestampPrecision precision = UtcTimestampPrecision.MILLIS; + + protected UtcTimeField(int field) { + super(field, LocalTime.now()); + } + + protected UtcTimeField(int field, LocalTime data) { + super(field, data); + } + + protected UtcTimeField(int field, LocalTime data, UtcTimestampPrecision precision) { + super(field, data); + this.precision = precision; + } + + public void setValue(LocalTime value) { + setObject(value); + } + + public LocalTime getValue() { + return getObject(); + } + + public boolean valueEquals(LocalTime value) { + return getValue().equals(value); + } + + public UtcTimestampPrecision getPrecision() { + return precision; + } +} diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java b/quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java index 73f73c670..f7ea1d73c 100644 --- a/quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java +++ b/quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java @@ -19,34 +19,52 @@ package quickfix; -import java.util.Date; +import java.time.LocalTime; /* * A time-valued message field. */ -public class UtcTimeOnlyField extends DateField { - private boolean includeMilliseconds = true; +public class UtcTimeOnlyField extends Field { + + private UtcTimestampPrecision precision = UtcTimestampPrecision.MILLIS; public UtcTimeOnlyField(int field) { - super(field); + super(field, LocalTime.now()); } - protected UtcTimeOnlyField(int field, Date data) { + protected UtcTimeOnlyField(int field, LocalTime data) { super(field, data); } + protected UtcTimeOnlyField(int field, LocalTime data, UtcTimestampPrecision precision) { + super(field, data); + this.precision = precision; + } + public UtcTimeOnlyField(int field, boolean includeMilliseconds) { - super(field); - this.includeMilliseconds = includeMilliseconds; + super(field, LocalTime.now()); + this.precision = includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS; } - protected UtcTimeOnlyField(int field, Date data, boolean includeMilliseconds) { + protected UtcTimeOnlyField(int field, LocalTime data, boolean includeMilliseconds) { super(field, data); - this.includeMilliseconds = includeMilliseconds; + this.precision = includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS; + } + + public UtcTimestampPrecision getPrecision() { + return precision; + } + + public void setValue(LocalTime value) { + setObject(value); + } + + public LocalTime getValue() { + return getObject(); } - boolean showMilliseconds() { - return includeMilliseconds; + public boolean valueEquals(LocalTime value) { + return getValue().equals(value); } } diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java b/quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java index fd09564e6..92c391630 100644 --- a/quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java +++ b/quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java @@ -19,33 +19,57 @@ package quickfix; -import java.util.Date; +import java.time.LocalDateTime; /** * A timestamp-valued message field (a timestamp has both a date and a time). */ -public class UtcTimeStampField extends DateField { - private boolean includeMilliseconds = true; +public class UtcTimeStampField extends Field { + + private UtcTimestampPrecision precision = UtcTimestampPrecision.MILLIS; public UtcTimeStampField(int field) { - super(field); + super(field, LocalDateTime.now()); + } + + protected UtcTimeStampField(int field, LocalDateTime data) { + super(field, data); } - protected UtcTimeStampField(int field, Date data) { + protected UtcTimeStampField(int field, LocalDateTime data, UtcTimestampPrecision precision) { super(field, data); + this.precision = precision; } public UtcTimeStampField(int field, boolean includeMilliseconds) { - super(field); - this.includeMilliseconds = includeMilliseconds; + super(field, LocalDateTime.now()); + this.precision = includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS; + } + + public UtcTimeStampField(int field, UtcTimestampPrecision precision) { + super(field, LocalDateTime.now()); + this.precision = precision; } - protected UtcTimeStampField(int field, Date data, boolean includeMilliseconds) { + protected UtcTimeStampField(int field, LocalDateTime data, boolean includeMilliseconds) { super(field, data); - this.includeMilliseconds = includeMilliseconds; + this.precision = includeMilliseconds ? UtcTimestampPrecision.MILLIS : UtcTimestampPrecision.SECONDS; + } + + public UtcTimestampPrecision getPrecision() { + return precision; + } + + public void setValue(LocalDateTime value) { + setObject(value); } - boolean showMilliseconds() { - return includeMilliseconds; + public LocalDateTime getValue() { + return getObject(); } + + public boolean valueEquals(LocalDateTime value) { + return getValue().equals(value); + } + } diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimestampPrecision.java b/quickfixj-core/src/main/java/quickfix/UtcTimestampPrecision.java new file mode 100644 index 000000000..fba92a6ad --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/UtcTimestampPrecision.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * 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; + +/** + * + * @author chrjohn + */ +public enum UtcTimestampPrecision { + SECONDS, MILLIS, MICROS, NANOS +} diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java index cc1d2bdcf..c19e85a4d 100644 --- a/quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java +++ b/quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java @@ -22,6 +22,7 @@ import java.text.DateFormat; import java.text.DateFormatSymbols; import java.text.SimpleDateFormat; +import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.TimeZone; @@ -34,6 +35,15 @@ protected static void assertLength(String value, int i, String type) throws Fiel } } + protected static void assertLength(String value, String type, int... lengths) throws FieldConvertError { + for (int length : lengths) { + if (value.length() == length) { + return; + } + } + throwFieldConvertError(value, type); + } + protected static void assertDigitSequence(String value, int i, int j, String type) throws FieldConvertError { for (int offset = i; offset < j; offset++) { @@ -69,5 +79,8 @@ protected DateFormat createDateFormat(String format) { sdf.setDateFormatSymbols(new DateFormatSymbols(Locale.US)); return sdf; } - + + protected static DateTimeFormatter createDateTimeFormat(String format) { + return DateTimeFormatter.ofPattern(format); + } } diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/DoubleConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/DoubleConverter.java index f3bc31a1d..ee8c05a88 100644 --- a/quickfixj-core/src/main/java/quickfix/field/converter/DoubleConverter.java +++ b/quickfixj-core/src/main/java/quickfix/field/converter/DoubleConverter.java @@ -19,21 +19,18 @@ package quickfix.field.converter; +import quickfix.FieldConvertError; +import quickfix.RuntimeError; + import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import quickfix.FieldConvertError; -import quickfix.RuntimeError; /** * Converts between a double and a String. */ public class DoubleConverter { - private static final Pattern DECIMAL_PATTERN = Pattern.compile("-?\\d*(\\.\\d*)?"); - private static final ThreadLocal THREAD_DECIMAL_FORMATS = new ThreadLocal(); + private static final ThreadLocal THREAD_DECIMAL_FORMATS = new ThreadLocal<>(); /** * Converts a double to a string with no padding. @@ -92,13 +89,25 @@ public static String convert(double d, int padding) { */ public static double convert(String value) throws FieldConvertError { try { - Matcher matcher = DECIMAL_PATTERN.matcher(value); - if (!matcher.matches()) { - throw new NumberFormatException(); - } - return Double.parseDouble(value); + return parseDouble(value); } catch (NumberFormatException e) { throw new FieldConvertError("invalid double value: " + value); } } + + private static double parseDouble(String value) { + if(value.length() == 0) throw new NumberFormatException(value); + boolean dot = false; int i = 0; + char c = value.charAt(i); + switch (c) { + case '-': i++; break; + case '+': throw new NumberFormatException(value); + } + for (; i < value.length(); i++) { + c = value.charAt(i); + if (!dot && c == '.') dot = true; + else if (c < '0' || c > '9') throw new NumberFormatException(value); + } + return Double.parseDouble(value); + } } diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java index 593bad044..cdaa67786 100644 --- a/quickfixj-core/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java +++ b/quickfixj-core/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java @@ -19,20 +19,28 @@ package quickfix.field.converter; +import quickfix.FieldConvertError; + import java.text.DateFormat; import java.text.ParseException; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Date; -import quickfix.FieldConvertError; - /** * Convert between a date and a String */ public class UtcDateOnlyConverter extends AbstractDateTimeConverter { + + static final String TYPE = "date"; + static final int DATE_LENGTH = 8; // SimpleDateFormats are not thread safe. A thread local is being // used to maintain high concurrency among multiple session threads - private static final ThreadLocal utcDateConverter = new ThreadLocal(); + private static final ThreadLocal UTC_DATE_CONVERTER = new ThreadLocal<>(); private final DateFormat dateFormat = createDateFormat("yyyyMMdd"); + private static final DateTimeFormatter FORMATTER_DATE = createDateTimeFormat("yyyyMMdd"); /** * Convert a date to a String ("YYYYMMDD") @@ -44,11 +52,15 @@ public static String convert(Date d) { return getFormatter().format(d); } + public static String convert(LocalDate d) { + return d.format(FORMATTER_DATE); + } + private static DateFormat getFormatter() { - UtcDateOnlyConverter converter = utcDateConverter.get(); + UtcDateOnlyConverter converter = UTC_DATE_CONVERTER.get(); if (converter == null) { converter = new UtcDateOnlyConverter(); - utcDateConverter.set(converter); + UTC_DATE_CONVERTER.set(converter); } return converter.dateFormat; } @@ -62,15 +74,39 @@ private static DateFormat getFormatter() { */ public static Date convert(String value) throws FieldConvertError { Date d = null; - String type = "date"; - assertLength(value, 8, type); - assertDigitSequence(value, 0, 8, type); + checkString(value); try { d = getFormatter().parse(value); } catch (ParseException e) { - throwFieldConvertError(value, type); + throwFieldConvertError(value, TYPE); } return d; } + public static LocalDate convertToLocalDate(String value) throws FieldConvertError { + checkString(value); + try { + return LocalDate.parse(value.substring(0, DATE_LENGTH), FORMATTER_DATE); + } catch (DateTimeParseException e) { + throwFieldConvertError(value, TYPE); + } + return null; + } + + private static void checkString(String value) throws FieldConvertError { + assertLength(value, DATE_LENGTH, TYPE); + assertDigitSequence(value, 0, DATE_LENGTH, TYPE); + } + + /** + * @param localDate + * @return a java.util.Date with date part filled from LocalDate. + */ + public static Date getDate(LocalDate localDate) { + if (localDate != null) { + return Date.from(localDate.atStartOfDay().atZone(ZoneOffset.UTC).toInstant()); + } + return null; + } + } diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java index 797bb36e2..fbc2d609f 100644 --- a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java +++ b/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java @@ -19,21 +19,39 @@ package quickfix.field.converter; +import quickfix.UtcTimestampPrecision; +import quickfix.FieldConvertError; + import java.text.DateFormat; import java.text.ParseException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Date; -import quickfix.FieldConvertError; - /** * Convert between a time and a String. */ public class UtcTimeOnlyConverter extends AbstractDateTimeConverter { + + static final String TYPE = "time"; + static final int LENGTH_INCL_SECONDS = 8; + static final int LENGTH_INCL_MILLIS = 12; + static final int LENGTH_INCL_MICROS = 15; + static final int LENGTH_INCL_NANOS = 18; + static final int LENGTH_INCL_PICOS = 21; + // SimpleDateFormats are not thread safe. A thread local is being // used to maintain high concurrency among multiple session threads - private static final ThreadLocal utcTimeConverter = new ThreadLocal(); + private static final ThreadLocal UTC_TIME_CONVERTER = new ThreadLocal<>(); private final DateFormat utcTimeFormat = createDateFormat("HH:mm:ss"); private final DateFormat utcTimeFormatMillis = createDateFormat("HH:mm:ss.SSS"); + private static final DateTimeFormatter FORMATTER_SECONDS = createDateTimeFormat("HH:mm:ss"); + private static final DateTimeFormatter FORMATTER_MILLIS = createDateTimeFormat("HH:mm:ss.SSS"); + private static final DateTimeFormatter FORMATTER_MICROS = createDateTimeFormat("HH:mm:ss.SSSSSS"); + private static final DateTimeFormatter FORMATTER_NANOS = createDateTimeFormat("HH:mm:ss.SSSSSSSSS"); /** * Convert a time (represented as a Date) to a String (HH:MM:SS or HH:MM:SS.SSS) @@ -46,11 +64,34 @@ public static String convert(Date d, boolean includeMilliseconds) { return getFormatter(includeMilliseconds).format(d); } + /** + * Convert a time (represented as LocalTime) to a String + * + * @param d the LocalTime with the time to convert + * @param precision controls whether seconds, milliseconds, microseconds or + * nanoseconds are included in the result + * @return a String representing the time. + */ + public static String convert(LocalTime d, UtcTimestampPrecision precision) { + switch (precision) { + case SECONDS: + return d.format(FORMATTER_SECONDS); + case MILLIS: + return d.format(FORMATTER_MILLIS); + case MICROS: + return d.format(FORMATTER_MICROS); + case NANOS: + return d.format(FORMATTER_NANOS); + default: + return d.format(FORMATTER_MILLIS); + } + } + private static DateFormat getFormatter(boolean includeMillis) { - UtcTimeOnlyConverter converter = utcTimeConverter.get(); + UtcTimeOnlyConverter converter = UTC_TIME_CONVERTER.get(); if (converter == null) { converter = new UtcTimeOnlyConverter(); - utcTimeConverter.set(converter); + UTC_TIME_CONVERTER.set(converter); } return includeMillis ? converter.utcTimeFormatMillis : converter.utcTimeFormat; } @@ -64,12 +105,49 @@ private static DateFormat getFormatter(boolean includeMillis) { */ public static Date convert(String value) throws FieldConvertError { Date d = null; + assertLength(value, TYPE, LENGTH_INCL_SECONDS, LENGTH_INCL_MILLIS, LENGTH_INCL_MICROS, LENGTH_INCL_NANOS, LENGTH_INCL_PICOS); try { - d = getFormatter(value.length() == 12).parse(value); + final boolean includeMillis = (value.length() >= LENGTH_INCL_MILLIS); + d = getFormatter(includeMillis).parse(includeMillis ? value.substring(0, LENGTH_INCL_MILLIS) : value); } catch (ParseException e) { - throwFieldConvertError(value, "time"); + throwFieldConvertError(value, TYPE); } return d; } + public static LocalTime convertToLocalTime(String value) throws FieldConvertError { + assertLength(value, TYPE, LENGTH_INCL_SECONDS, LENGTH_INCL_MILLIS, LENGTH_INCL_MICROS, LENGTH_INCL_NANOS, LENGTH_INCL_PICOS); + try { + int length = value.length(); + switch (length) { + case LENGTH_INCL_SECONDS: + return LocalTime.parse(value, FORMATTER_SECONDS); + case LENGTH_INCL_MILLIS: + return LocalTime.parse(value, FORMATTER_MILLIS); + case LENGTH_INCL_MICROS: + return LocalTime.parse(value, FORMATTER_MICROS); + case LENGTH_INCL_NANOS: + return LocalTime.parse(value, FORMATTER_NANOS); + case LENGTH_INCL_PICOS: + return LocalTime.parse(value.substring(0, LENGTH_INCL_NANOS), FORMATTER_NANOS); + default: + throwFieldConvertError(value, TYPE); + } + } catch (DateTimeParseException e) { + throwFieldConvertError(value, TYPE); + } + return null; + } + + /** + * @param localTime + * @return a java.util.Date with time part filled from LocalTime (truncated to milliseconds). + */ + public static Date getDate(LocalTime localTime) { + if (localTime != null) { + return Date.from(localTime.atDate(LocalDate.ofEpochDay(0)).atZone(ZoneOffset.UTC).toInstant()); + } + return null; + } + } diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java index 8b2cfcc91..d792ad2e0 100644 --- a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java +++ b/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java @@ -1,4 +1,4 @@ -/******************************************************************************* +/** ***************************************************************************** * Copyright (c) quickfixengine.org All rights reserved. * * This file is part of the QuickFIX FIX Engine @@ -15,57 +15,101 @@ * * Contact ask@quickfixengine.org if any conditions of this licensing * are not clear to you. - ******************************************************************************/ - + ***************************************************************************** */ package quickfix.field.converter; +import quickfix.UtcTimestampPrecision; +import org.quickfixj.SimpleCache; +import quickfix.FieldConvertError; +import quickfix.SystemTime; + import java.text.DateFormat; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; -import java.util.concurrent.*; - -import quickfix.FieldConvertError; -import quickfix.SystemTime; /** * Convert between a timestamp and a String. A timestamp includes both a date * and a time. */ public class UtcTimestampConverter extends AbstractDateTimeConverter { - private static final ThreadLocal utcTimestampConverter = new ThreadLocal(); + + static final String TYPE = "timestamp"; + static final int LENGTH_INCL_SECONDS = 17; + static final int LENGTH_INCL_MILLIS = 21; + static final int LENGTH_INCL_MICROS = 24; + static final int LENGTH_INCL_NANOS = 27; + static final int LENGTH_INCL_PICOS = 30; + + private static final ThreadLocal UTC_TIMESTAMP_CONVERTER = new ThreadLocal<>(); + private static final SimpleCache DATE_CACHE = new SimpleCache<>(dateString -> { + final Calendar c = new GregorianCalendar(1970, 0, 1, 0, 0, 0); + c.setTimeZone(SystemTime.UTC_TIMEZONE); + final int year = Integer.parseInt(dateString.substring(0, 4)); + final int month = Integer.parseInt(dateString.substring(4, 6)); + final int day = Integer.parseInt(dateString.substring(6, 8)); + c.set(year, month - 1, day); + return c.getTimeInMillis(); + }); + private final DateFormat utcTimestampFormat = createDateFormat("yyyyMMdd-HH:mm:ss"); private final DateFormat utcTimestampFormatMillis = createDateFormat("yyyyMMdd-HH:mm:ss.SSS"); - private final static ConcurrentHashMap dateCache = new ConcurrentHashMap(); + private static final DateTimeFormatter FORMATTER_SECONDS = createDateTimeFormat("yyyyMMdd-HH:mm:ss"); + private static final DateTimeFormatter FORMATTER_MILLIS = createDateTimeFormat("yyyyMMdd-HH:mm:ss.SSS"); + private static final DateTimeFormatter FORMATTER_MICROS = createDateTimeFormat("yyyyMMdd-HH:mm:ss.SSSSSS"); + private static final DateTimeFormatter FORMATTER_NANOS = createDateTimeFormat("yyyyMMdd-HH:mm:ss.SSSSSSSSS"); /** * Convert a timestamp (represented as a Date) to a String. * * @param d the date to convert - * @param includeMilliseconds controls whether milliseconds are included in the result + * @param includeMilliseconds controls whether milliseconds are included in + * the result * @return the formatted timestamp */ public static String convert(Date d, boolean includeMilliseconds) { return getFormatter(includeMilliseconds).format(d); } + /** + * Convert a timestamp (represented as a LocalDateTime) to a String. + * + * @param d the date to convert + * @param precision controls whether seconds, milliseconds, microseconds or + * nanoseconds are included in the result + * @return the formatted timestamp + */ + public static String convert(LocalDateTime d, UtcTimestampPrecision precision) { + switch (precision) { + case SECONDS: + return d.format(FORMATTER_SECONDS); + case MILLIS: + return d.format(FORMATTER_MILLIS); + case MICROS: + return d.format(FORMATTER_MICROS); + case NANOS: + return d.format(FORMATTER_NANOS); + default: + return d.format(FORMATTER_MILLIS); + } + } + private static DateFormat getFormatter(boolean includeMillis) { - UtcTimestampConverter converter = utcTimestampConverter.get(); + UtcTimestampConverter converter = UTC_TIMESTAMP_CONVERTER.get(); if (converter == null) { converter = new UtcTimestampConverter(); - utcTimestampConverter.set(converter); + UTC_TIMESTAMP_CONVERTER.set(converter); } return includeMillis ? converter.utcTimestampFormatMillis : converter.utcTimestampFormat; } - // - // Performance optimization: the calendar for the start of the day is cached. - // The time is converted to millis and then added to the millis specified by - // the base calendar. - // - /** * Convert a timestamp string into a Date. + * Date has up to millisecond precision. * * @param value the timestamp String * @return the parsed timestamp @@ -73,48 +117,92 @@ private static DateFormat getFormatter(boolean includeMillis) { */ public static Date convert(String value) throws FieldConvertError { verifyFormat(value); - long timeOffset = (parseLong(value.substring(9, 11)) * 3600000L) - + (parseLong(value.substring(12, 14)) * 60000L) - + (parseLong(value.substring(15, 17)) * 1000L); - if (value.length() == 21) { - timeOffset += parseLong(value.substring(18, 21)); + long timeOffset = getTimeOffsetSeconds(value); + if (value.length() >= LENGTH_INCL_MILLIS) { // format has already been verified + // accept up to picosenconds but parse only up to milliseconds + timeOffset += parseLong(value.substring(18, LENGTH_INCL_MILLIS)); } return new Date(getMillisForDay(value) + timeOffset); } - private static Long getMillisForDay(String value) { - String dateString = value.substring(0, 8); - Long millis = dateCache.get(dateString); - if (millis == null) { - Calendar c = new GregorianCalendar(1970, 0, 1, 0, 0, 0); - c.setTimeZone(SystemTime.UTC_TIMEZONE); - int year = Integer.parseInt(value.substring(0, 4)); - int month = Integer.parseInt(value.substring(4, 6)); - int day = Integer.parseInt(value.substring(6, 8)); - c.set(year, month - 1, day); - millis = c.getTimeInMillis(); - dateCache.put(dateString, c.getTimeInMillis()); + /** + * Convert a timestamp string into a LocalDateTime object. + * LocalDateTime has up to nanosecond precision. + * + * @param value the timestamp String + * @return the parsed timestamp + * @exception FieldConvertError raised if timestamp is an incorrect format. + */ + public static LocalDateTime convertToLocalDateTime(String value) throws FieldConvertError { + verifyFormat(value); + int length = value.length(); + try { + switch (length) { + case LENGTH_INCL_SECONDS: + return LocalDateTime.parse(value, FORMATTER_SECONDS); + case LENGTH_INCL_MILLIS: + return LocalDateTime.parse(value, FORMATTER_MILLIS); + case LENGTH_INCL_MICROS: + return LocalDateTime.parse(value, FORMATTER_MICROS); + case LENGTH_INCL_NANOS: + case LENGTH_INCL_PICOS: + return LocalDateTime.parse(value.substring(0, LENGTH_INCL_NANOS), FORMATTER_NANOS); + default: + throwFieldConvertError(value, TYPE); + } + } catch (DateTimeParseException e) { + throwFieldConvertError(value, TYPE); } - return millis; + return null; + } + + private static Long getMillisForDay(String value) { + // Performance optimization: the calendar for the start of the day is cached. + return DATE_CACHE.computeIfAbsent(value.substring(0, 8)); + } + + private static long getTimeOffsetSeconds(String value) { + long timeOffset = (parseLong(value.substring(9, 11)) * 3600000L) + + (parseLong(value.substring(12, 14)) * 60000L) + + (parseLong(value.substring(15, LENGTH_INCL_SECONDS)) * 1000L); + return timeOffset; } private static void verifyFormat(String value) throws FieldConvertError { - String type = "timestamp"; - if (value.length() != 17 && value.length() != 21) { - throwFieldConvertError(value, type); + assertLength(value, TYPE, LENGTH_INCL_SECONDS, LENGTH_INCL_MILLIS, LENGTH_INCL_MICROS, LENGTH_INCL_NANOS, LENGTH_INCL_PICOS); + assertDigitSequence(value, 0, 8, TYPE); + assertSeparator(value, 8, '-', TYPE); + assertDigitSequence(value, 9, 11, TYPE); + assertSeparator(value, 11, ':', TYPE); + assertDigitSequence(value, 12, 14, TYPE); + assertSeparator(value, 14, ':', TYPE); + assertDigitSequence(value, 15, LENGTH_INCL_SECONDS, TYPE); + if (value.length() == LENGTH_INCL_MILLIS) { + assertSeparator(value, LENGTH_INCL_SECONDS, '.', TYPE); + assertDigitSequence(value, 18, LENGTH_INCL_MILLIS, TYPE); + } else if (value.length() == LENGTH_INCL_MICROS) { + assertSeparator(value, LENGTH_INCL_SECONDS, '.', TYPE); + assertDigitSequence(value, 18, LENGTH_INCL_MICROS, TYPE); + } else if (value.length() == LENGTH_INCL_NANOS) { + assertSeparator(value, LENGTH_INCL_SECONDS, '.', TYPE); + assertDigitSequence(value, 18, LENGTH_INCL_NANOS, TYPE); + } else if (value.length() == LENGTH_INCL_PICOS) { + assertSeparator(value, LENGTH_INCL_SECONDS, '.', TYPE); + assertDigitSequence(value, 18, LENGTH_INCL_PICOS, TYPE); + } else if (value.length() != LENGTH_INCL_SECONDS) { + throwFieldConvertError(value, TYPE); } - assertDigitSequence(value, 0, 8, type); - assertSeparator(value, 8, '-', type); - assertDigitSequence(value, 9, 11, type); - assertSeparator(value, 11, ':', type); - assertDigitSequence(value, 12, 14, type); - assertSeparator(value, 14, ':', type); - assertDigitSequence(value, 15, 17, type); - if (value.length() == 21) { - assertSeparator(value, 17, '.', type); - assertDigitSequence(value, 18, 21, type); - } else if (value.length() != 17) { - throwFieldConvertError(value, type); + } + + /** + * @param localDateTime + * @return a java.util.Date filled from LocalDateTime (truncated to milliseconds). + */ + public static Date getDate(LocalDateTime localDateTime) { + if (localDateTime != null) { + return Date.from(localDateTime.toInstant(ZoneOffset.UTC)); } + return null; } + } diff --git a/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java b/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java index cf3a026a0..5e60c0e1d 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java +++ b/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java @@ -87,10 +87,14 @@ public void exceptionCaught(IoSession ioSession, Throwable cause) throws Excepti ioSession.closeNow(); } } finally { - ioSession.setAttribute("QFJ_RESET_IO_CONNECTOR", Boolean.TRUE); + ioSession.setAttribute(SessionConnector.QFJ_RESET_IO_CONNECTOR, Boolean.TRUE); } } else { - log.error(reason, cause); + if (quickFixSession != null) { + LogUtil.logThrowable(quickFixSession.getLog(), reason, cause); + } else { + log.error(reason, cause); + } } } @@ -121,20 +125,21 @@ public void messageReceived(IoSession ioSession, Object message) throws Exceptio SessionID remoteSessionID = MessageUtils.getReverseSessionID(messageString); Session quickFixSession = findQFSession(ioSession, remoteSessionID); if (quickFixSession != null) { - quickFixSession.getLog().onIncoming(messageString); + final Log sessionLog = quickFixSession.getLog(); + sessionLog.onIncoming(messageString); try { Message fixMessage = parse(quickFixSession, messageString); processMessage(ioSession, fixMessage); } catch (InvalidMessage e) { if (MsgType.LOGON.equals(MessageUtils.getMessageType(messageString))) { - log.error("Invalid LOGON message, disconnecting: " + e.getMessage()); + sessionLog.onErrorEvent("Invalid LOGON message, disconnecting: " + e.getMessage()); ioSession.closeNow(); } else { - log.error("Invalid message: " + e.getMessage()); + sessionLog.onErrorEvent("Invalid message: " + e.getMessage()); } } } else { - log.error("Disconnecting; received message for unknown session: " + messageString); + log.error("Disconnecting; received message for unknown session: {}", messageString); ioSession.closeNow(); } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/EventHandlingStrategy.java b/quickfixj-core/src/main/java/quickfix/mina/EventHandlingStrategy.java index c5149f843..573288f17 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/EventHandlingStrategy.java +++ b/quickfixj-core/src/main/java/quickfix/mina/EventHandlingStrategy.java @@ -28,15 +28,14 @@ * it only handles message reception events. */ public interface EventHandlingStrategy { - /** * Constant indicating how long we wait for an incoming message. After * thread has been asked to stop, it can take up to this long to terminate. */ - static final long THREAD_WAIT_FOR_MESSAGE_MS = 250; + long THREAD_WAIT_FOR_MESSAGE_MS = 250; // will be put to the eventQueue to signal a disconnection - public static final Message END_OF_STREAM = new Message(); + Message END_OF_STREAM = new Message(); void onMessage(Session quickfixSession, Message message); diff --git a/quickfixj-core/src/main/java/quickfix/mina/IoSessionResponder.java b/quickfixj-core/src/main/java/quickfix/mina/IoSessionResponder.java index d7351d5a3..1c32dcb36 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/IoSessionResponder.java +++ b/quickfixj-core/src/main/java/quickfix/mina/IoSessionResponder.java @@ -64,11 +64,11 @@ public boolean send(String data) { if (synchronousWrites) { try { if (!future.awaitUninterruptibly(synchronousWriteTimeout)) { - log.error("Synchronous write timed out after " + synchronousWriteTimeout + "ms"); + log.error("Synchronous write timed out after {}ms", synchronousWriteTimeout); return false; } } catch (RuntimeException e) { - log.error("Synchronous write failed: " + e.getMessage()); + log.error("Synchronous write failed: {}", e.getMessage()); return false; } } @@ -83,7 +83,7 @@ public void disconnect() { // close event from being processed by this thread (if // this thread is the MINA IO processor thread. ioSession.closeOnFlush(); - ioSession.setAttribute("QFJ_RESET_IO_CONNECTOR", Boolean.TRUE); + ioSession.setAttribute(SessionConnector.QFJ_RESET_IO_CONNECTOR, Boolean.TRUE); } @Override @@ -95,4 +95,7 @@ public String getRemoteAddress() { return null; } + IoSession getIoSession() { + return ioSession; + } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java b/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java index 7d9b33aba..b653f15eb 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java +++ b/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java @@ -19,21 +19,20 @@ package quickfix.mina; -import java.net.SocketException; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; - import org.apache.mina.core.session.IoSession; import org.apache.mina.core.session.IoSessionConfig; import org.apache.mina.transport.socket.SocketSessionConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import quickfix.FieldConvertError; import quickfix.field.converter.BooleanConverter; import quickfix.field.converter.IntConverter; +import java.net.SocketException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + /** * This class holds the QFJ settings information related to networking options. */ @@ -65,7 +64,7 @@ public class NetworkingOptions { public static final String IPTOC_RELIABILITY = "IPTOS_RELIABILITY"; public static final String IPTOC_THROUGHPUT = "IPTOS_THROUGHPUT"; public static final String IPTOC_LOWDELAY = "IPTOS_LOWDELAY"; - public static final Map trafficClasses = new HashMap(); + public static final Map trafficClasses = new HashMap<>(); static { trafficClasses.put(IPTOC_LOWCOST, 0x02); @@ -101,8 +100,7 @@ public NetworkingOptions(Properties properties) throws FieldConvertError { } } trafficClassSetting = trafficClassBits; - log.info("Socket option: " + SETTING_SOCKET_TRAFFIC_CLASS + "= 0x" - + Integer.toHexString(trafficClassBits) + " (" + trafficClassEnumString + ")"); + log.info("Socket option: {}= 0x{} ({})", SETTING_SOCKET_TRAFFIC_CLASS, Integer.toHexString(trafficClassBits), trafficClassEnumString); } trafficClass = trafficClassSetting; @@ -117,7 +115,7 @@ private Boolean getBoolean(Properties properties, String key, Boolean defaultVal private void logOption(String key, Object value) { if (value != null) { - log.info("Socket option: " + key + "=" + value); + log.info("Socket option: {}={}", key, value); } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java b/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java index 640247b74..0e9bfaf6d 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java @@ -21,14 +21,28 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.HashMap; + import org.apache.mina.core.service.IoAcceptor; import org.apache.mina.core.service.IoConnector; +import org.apache.mina.transport.socket.SocketConnector; import org.apache.mina.transport.socket.nio.NioSocketAcceptor; import org.apache.mina.transport.socket.nio.NioSocketConnector; import org.apache.mina.transport.vmpipe.VmPipeAcceptor; import org.apache.mina.transport.vmpipe.VmPipeAddress; import org.apache.mina.transport.vmpipe.VmPipeConnector; +import org.apache.mina.proxy.ProxyConnector; +import org.apache.mina.proxy.handlers.ProxyRequest; +import org.apache.mina.proxy.handlers.http.HttpAuthenticationMethods; +import org.apache.mina.proxy.handlers.http.HttpProxyConstants; +import org.apache.mina.proxy.handlers.http.HttpProxyRequest; +import org.apache.mina.proxy.handlers.socks.SocksProxyConstants; +import org.apache.mina.proxy.handlers.socks.SocksProxyRequest; +import org.apache.mina.proxy.session.ProxyIoSession; + import quickfix.ConfigError; import quickfix.RuntimeError; @@ -58,7 +72,7 @@ public static String getTypeString(int type) { public static SocketAddress createSocketAddress(int transportType, String host, int port) throws ConfigError { - if (transportType == SOCKET) { + if (transportType == SOCKET || transportType == PROXY) { return host != null ? new InetSocketAddress(host, port) : new InetSocketAddress(port); } else if (transportType == VM_PIPE) { return new VmPipeAddress(port); @@ -102,6 +116,108 @@ public static IoAcceptor createIoAcceptor(int transportType) { } } + public static ProxyConnector createIoProxyConnector(SocketConnector socketConnector, + InetSocketAddress address, + InetSocketAddress proxyAddress, + String proxyType, + String proxyVersion, + String proxyUser, + String proxyPassword, + String proxyDomain, + String proxyWorkstation ) throws ConfigError { + + // Create proxy connector. + ProxyRequest req; + + ProxyConnector connector = new ProxyConnector(socketConnector); + connector.setConnectTimeoutMillis(5000); + + if (proxyType.equalsIgnoreCase("http")) { + req = createHttpProxyRequest(address, proxyVersion, proxyUser, proxyPassword, proxyDomain, proxyWorkstation); + } else if (proxyType.equalsIgnoreCase("socks")) { + req = createSocksProxyRequest(address, proxyVersion, proxyUser, proxyPassword); + } else { + throw new ConfigError("Proxy type must be http or socks"); + } + + ProxyIoSession proxyIoSession = new ProxyIoSession(proxyAddress, req); + + List l = new ArrayList<>(); + l.add(HttpAuthenticationMethods.NO_AUTH); + l.add(HttpAuthenticationMethods.DIGEST); + l.add(HttpAuthenticationMethods.BASIC); + + proxyIoSession.setPreferedOrder(l); + connector.setProxyIoSession(proxyIoSession); + + return connector; + } + + + private static ProxyRequest createHttpProxyRequest(InetSocketAddress address, + String proxyVersion, + String proxyUser, + String proxyPassword, + String proxyDomain, + String proxyWorkstation) { + HashMap props = new HashMap<>(); + props.put(HttpProxyConstants.USER_PROPERTY, proxyUser); + props.put(HttpProxyConstants.PWD_PROPERTY, proxyPassword); + if (proxyDomain != null && proxyWorkstation != null) { + props.put(HttpProxyConstants.DOMAIN_PROPERTY, proxyDomain); + props.put(HttpProxyConstants.WORKSTATION_PROPERTY, proxyWorkstation); + } + + HttpProxyRequest req = new HttpProxyRequest(address); + req.setProperties(props); + if (proxyVersion != null && proxyVersion.equalsIgnoreCase("1.1")) { + req.setHttpVersion(HttpProxyConstants.HTTP_1_1); + } else { + req.setHttpVersion(HttpProxyConstants.HTTP_1_0); + } + + return req; + } + + + private static ProxyRequest createSocksProxyRequest(InetSocketAddress address, + String proxyVersion, + String proxyUser, + String proxyPassword) throws ConfigError { + SocksProxyRequest req; + if (proxyVersion.equalsIgnoreCase("4")) { + req = new SocksProxyRequest( + SocksProxyConstants.SOCKS_VERSION_4, + SocksProxyConstants.ESTABLISH_TCPIP_STREAM, + address, + proxyUser); + + } else if (proxyVersion.equalsIgnoreCase("4a")) { + req = new SocksProxyRequest( + SocksProxyConstants.ESTABLISH_TCPIP_STREAM, + address.getAddress().getHostAddress(), + address.getPort(), + proxyUser); + + } else if (proxyVersion.equalsIgnoreCase("5")) { + req = new SocksProxyRequest( + SocksProxyConstants.SOCKS_VERSION_5, + SocksProxyConstants.ESTABLISH_TCPIP_STREAM, + address, + proxyUser); + + } else { + throw new ConfigError("SOCKS ProxyType must be 4,4a or 5"); + } + + if (proxyPassword != null) { + req.setPassword(proxyPassword); + } + + return req; + } + + public static IoConnector createIoConnector(SocketAddress address) throws ConfigError { if (address instanceof InetSocketAddress) { return new NioSocketConnector(); diff --git a/quickfixj-core/src/main/java/quickfix/mina/QueueTracker.java b/quickfixj-core/src/main/java/quickfix/mina/QueueTracker.java new file mode 100644 index 000000000..aa2928e5e --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/mina/QueueTracker.java @@ -0,0 +1,10 @@ +package quickfix.mina; + +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +interface QueueTracker { + void put(E e) throws InterruptedException; + E poll(long timeout, TimeUnit unit) throws InterruptedException; + int drainTo(Collection collection); +} diff --git a/quickfixj-core/src/main/java/quickfix/mina/QueueTrackers.java b/quickfixj-core/src/main/java/quickfix/mina/QueueTrackers.java new file mode 100644 index 000000000..00f1ecb3b --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/mina/QueueTrackers.java @@ -0,0 +1,89 @@ +package quickfix.mina; + +import org.apache.mina.core.session.IoSession; +import quickfix.Responder; +import quickfix.Session; + +import java.util.Collection; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import static java.lang.String.format; + +/** + * Queue trackers factory methods + */ +final class QueueTrackers { + private static final String LOWER_WATERMARK_FMT = "inbound queue size < lower watermark (%d), socket reads resumed"; + private static final String UPPER_WATERMARK_FMT = "inbound queue size > upper watermark (%d), socket reads suspended"; + + /** + * Watermarks-based queue tracker + */ + static WatermarkTracker newMultiSessionWatermarkTracker( + BlockingQueue queue, + long lowerWatermark, long upperWatermark, + Function classifier) { + return WatermarkTracker.newMulti(queue, lowerWatermark, upperWatermark, classifier, + qfSession -> resumeReads(qfSession, (int)lowerWatermark), + qfSession -> suspendReads(qfSession, (int)upperWatermark)); + } + + /** + * Default no-op queue tracker + */ + static QueueTracker newDefaultQueueTracker(BlockingQueue queue) { + return new QueueTracker() { + @Override + public void put(E e) throws InterruptedException { + queue.put(e); + } + + @Override + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + return queue.poll(timeout, unit); + } + + @Override + public int drainTo(Collection collection) { + return queue.drainTo(collection); + } + }; + } + + private static IoSession lookupIoSession(Session qfSession) { + final Responder responder = qfSession.getResponder(); + + if (responder instanceof IoSessionResponder) { + return ((IoSessionResponder)responder).getIoSession(); + } else { + return null; + } + } + + private static void resumeReads(Session qfSession, int queueLowerWatermark) { + final IoSession ioSession = lookupIoSession(qfSession); + if (ioSession != null && ioSession.isReadSuspended()) { + ioSession.resumeRead(); + qfSession.getLog().onEvent(format(LOWER_WATERMARK_FMT, queueLowerWatermark)); + } + } + + private static void suspendReads(Session qfSession, int queueUpperWatermark) { + final IoSession ioSession = lookupIoSession(qfSession); + if (ioSession != null && !ioSession.isReadSuspended()) { + ioSession.suspendRead(); + qfSession.getLog().onEvent(format(UPPER_WATERMARK_FMT, queueUpperWatermark)); + } + } + + static WatermarkTracker newSingleSessionWatermarkTracker( + BlockingQueue queue, + long lowerWatermark, long upperWatermark, + Session qfSession) { + return WatermarkTracker.newMono(queue, lowerWatermark, upperWatermark, + () -> resumeReads(qfSession, (int)lowerWatermark), + () -> suspendReads(qfSession, (int)upperWatermark)); + } +} diff --git a/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java b/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java index d09bf79c8..eeeae51b0 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java +++ b/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java @@ -19,6 +19,20 @@ package quickfix.mina; +import org.apache.mina.core.filterchain.IoFilterChainBuilder; +import org.apache.mina.core.session.IoSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quickfix.ConfigError; +import quickfix.Connector; +import quickfix.ExecutorFactory; +import quickfix.FieldConvertError; +import quickfix.Session; +import quickfix.SessionFactory; +import quickfix.SessionID; +import quickfix.SessionSettings; +import quickfix.field.converter.IntConverter; + import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; @@ -29,25 +43,15 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; - -import org.apache.mina.core.filterchain.IoFilterChainBuilder; -import org.apache.mina.core.session.IoSession; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import quickfix.ConfigError; -import quickfix.Connector; -import quickfix.FieldConvertError; -import quickfix.Session; -import quickfix.SessionFactory; -import quickfix.SessionID; -import quickfix.SessionSettings; -import quickfix.field.converter.IntConverter; +import org.apache.mina.core.future.CloseFuture; +import org.apache.mina.core.service.IoService; /** * An abstract base class for acceptors and initiators. Provides support for common functionality and also serves as an @@ -56,7 +60,8 @@ public abstract class SessionConnector implements Connector { protected static final int DEFAULT_QUEUE_CAPACITY = 10000; public static final String SESSIONS_PROPERTY = "sessions"; - public final static String QF_SESSION = "QF_SESSION"; + public static final String QF_SESSION = "QF_SESSION"; + public static final String QFJ_RESET_IO_CONNECTOR = "QFJ_RESET_IO_CONNECTOR"; protected final Logger log = LoggerFactory.getLogger(getClass()); @@ -70,6 +75,9 @@ public abstract class SessionConnector implements Connector { private ScheduledFuture sessionTimerFuture; private IoFilterChainBuilder ioFilterChainBuilder; + protected Executor longLivedExecutor; + protected Executor shortLivedExecutor; + public SessionConnector(SessionSettings settings, SessionFactory sessionFactory) throws ConfigError { this.settings = settings; this.sessionFactory = sessionFactory; @@ -78,6 +86,28 @@ public SessionConnector(SessionSettings settings, SessionFactory sessionFactory) } } + /** + *

    + * Supplies the Executors to be used for all message processing and timer activities. This will override the default + * behavior which uses internally created Threads. This enables scenarios such as a ResourceAdapter to supply the + * WorkManager (when adapted to the Executor API) so that all Application call-backs occur on container managed + * threads. + *

    + *

    + * If using external Executors, this method should be called immediately after the constructor. Once set, the + * Executors cannot be changed. + *

    + * + * @param executorFactory See {@link ExecutorFactory} for detailed requirements. + */ + public void setExecutorFactory(ExecutorFactory executorFactory) { + if (longLivedExecutor != null || shortLivedExecutor!=null) { + throw new IllegalStateException("Optional ExecutorFactory has already been set. It cannot be changed once set."); + } + longLivedExecutor = executorFactory.getLongLivedExecutor(); + shortLivedExecutor = executorFactory.getShortLivedExecutor(); + } + public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); } @@ -91,6 +121,15 @@ protected void setSessions(Map sessions) { propertyChangeSupport.firePropertyChange(SESSIONS_PROPERTY, null, sessions); } + /** + * Will remove all Sessions from the SessionConnector's Session map. + * Please make sure that these Sessions were unregistered before via + * Session.unregisterSessions(). + */ + protected void clearConnectorSessions() { + this.sessions.clear(); + } + /** * Get the list of session managed by this connector. * @@ -98,7 +137,7 @@ protected void setSessions(Map sessions) { * @see quickfix.Session */ public List getManagedSessions() { - return new ArrayList(sessions.values()); + return new ArrayList<>(sessions.values()); } /** @@ -118,18 +157,18 @@ protected Map getSessionMap() { * @return list of session identifiers */ public ArrayList getSessions() { - return new ArrayList(sessions.keySet()); + return new ArrayList<>(sessions.keySet()); } public void addDynamicSession(Session inSession) { sessions.put(inSession.getSessionID(), inSession); - log.debug("adding session for " + inSession.getSessionID()); + log.debug("adding session for {}", inSession.getSessionID()); propertyChangeSupport.firePropertyChange(SESSIONS_PROPERTY, null, sessions); } public void removeDynamicSession(SessionID inSessionID) { sessions.remove(inSessionID); - log.debug("removing session for " + inSessionID); + log.debug("removing session for {}", inSessionID); propertyChangeSupport.firePropertyChange(SESSIONS_PROPERTY, null, sessions); } @@ -167,8 +206,27 @@ public boolean isLoggedOn() { return true; } + /** + * Check if we have at least one session and that at least one session is logged on. + * + * @return false if no sessions exist or all sessions are logged off, true otherwise + */ + //visible for testing only + boolean anyLoggedOn() { + // if no session, not logged on + if (sessions.isEmpty()) + return false; + for (Session session : sessions.values()) { + // at least one session logged on + if (session.isLoggedOn()) + return true; + } + // no sessions are logged on + return false; + } + private Set getLoggedOnSessions() { - Set loggedOnSessions = new HashSet(sessions.size()); + Set loggedOnSessions = new HashSet<>(sessions.size()); for (Session session : sessions.values()) { if (session.isLoggedOn()) { loggedOnSessions.add(session); @@ -191,21 +249,21 @@ protected void logoutAllSessions(boolean forceDisconnect) { } } - if (forceDisconnect && isLoggedOn()) { - for (Session session : sessions.values()) { - try { - if (session.isLoggedOn()) { - session.disconnect("Forcibly disconnecting session", false); + if (anyLoggedOn()) { + if (forceDisconnect) { + for (Session session : sessions.values()) { + try { + if (session.isLoggedOn()) { + session.disconnect("Forcibly disconnecting session", false); + } + } catch (Throwable e) { + logError(session.getSessionID(), null, "Error during disconnect", e); } - } catch (Throwable e) { - logError(session.getSessionID(), null, "Error during disconnect", e); } + } else { + waitForLogout(); } } - - if (!forceDisconnect) { - waitForLogout(); - } } protected void waitForLogout() { @@ -254,14 +312,18 @@ private String getLogSuffix(SessionID sessionID, IoSession protocolSession) { } protected void startSessionTimer() { - sessionTimerFuture = scheduledExecutorService.scheduleAtFixedRate(new SessionTimerTask(), 0, 1000L, + Runnable timerTask = new SessionTimerTask(); + if (shortLivedExecutor != null) { + timerTask = new DelegatingTask(timerTask, shortLivedExecutor); + } + sessionTimerFuture = scheduledExecutorService.scheduleAtFixedRate(timerTask, 0, 1000L, TimeUnit.MILLISECONDS); log.info("SessionTimer started"); } protected void stopSessionTimer() { if (sessionTimerFuture != null) { - if (sessionTimerFuture.cancel(false)) + if (sessionTimerFuture.cancel(true)) log.info("SessionTimer canceled"); } } @@ -286,6 +348,59 @@ public void run() { } } + /** + * Delegates QFJ Timer Task to an Executor and blocks the QFJ Timer Thread until + * the Task execution completes. + */ + static final class DelegatingTask implements Runnable { + + private final BlockingSupportTask delegate; + private final Executor executor; + + DelegatingTask(Runnable delegate, Executor executor) { + this.delegate = new BlockingSupportTask(delegate); + this.executor = executor; + } + + @Override + public void run() { + executor.execute(delegate); + try { + delegate.await(); + } catch (InterruptedException e) { + } + } + + static final class BlockingSupportTask implements Runnable { + + private final CountDownLatch latch = new CountDownLatch(1); + private final Runnable delegate; + + BlockingSupportTask(Runnable delegate) { + this.delegate = delegate; + } + + @Override + public void run() { + Thread currentThread = Thread.currentThread(); + String threadName = currentThread.getName(); + try { + currentThread.setName("QFJ Timer (" + threadName + ")"); + delegate.run(); + } finally { + latch.countDown(); + currentThread.setName(threadName); + } + } + + void await() throws InterruptedException { + latch.await(); + } + + } + + } + private static class QFTimerThreadFactory implements ThreadFactory { public Thread newThread(Runnable runnable) { @@ -310,4 +425,33 @@ public void setIoFilterChainBuilder(IoFilterChainBuilder ioFilterChainBuilder) { protected IoFilterChainBuilder getIoFilterChainBuilder() { return ioFilterChainBuilder; } + + /** + * Closes all managed sessions of an Initiator/Acceptor. + * + * @param ioService Acceptor or Initiator implementation + * @param awaitTermination whether to wait for underlying ExecutorService to terminate + * @param logger used for logging WARNING when IoSession could not be closed + */ + public static void closeManagedSessionsAndDispose(IoService ioService, boolean awaitTermination, Logger logger) { + Map managedSessions = ioService.getManagedSessions(); + for (IoSession ioSession : managedSessions.values()) { + if (!ioSession.isClosing()) { + CloseFuture closeFuture = ioSession.closeNow(); + boolean completed = false; + try { + completed = closeFuture.await(1000, TimeUnit.MILLISECONDS); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + if (!completed) { + logger.warn("Could not close IoSession {}", ioSession); + } + } + } + if (!ioService.isDisposing()) { + ioService.dispose(awaitTermination); + } + } + } diff --git a/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java b/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java index 1f46fa3a1..b14f4c653 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java +++ b/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java @@ -25,23 +25,42 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import static quickfix.mina.QueueTrackers.newDefaultQueueTracker; +import static quickfix.mina.QueueTrackers.newMultiSessionWatermarkTracker; + /** * Processes messages for all sessions in a single thread. */ public class SingleThreadedEventHandlingStrategy implements EventHandlingStrategy { public static final String MESSAGE_PROCESSOR_THREAD_NAME = "QFJ Message Processor"; private final BlockingQueue eventQueue; + private final QueueTracker queueTracker; private final SessionConnector sessionConnector; - private volatile Thread messageProcessingThread; + private volatile ThreadAdapter messageProcessingThread; private volatile boolean isStopped; + private Executor executor; private long stopTime = 0L; public SingleThreadedEventHandlingStrategy(SessionConnector connector, int queueCapacity) { sessionConnector = connector; - eventQueue = new LinkedBlockingQueue(queueCapacity); + eventQueue = new LinkedBlockingQueue<>(queueCapacity); + queueTracker = newDefaultQueueTracker(eventQueue); + } + + public SingleThreadedEventHandlingStrategy(SessionConnector connector, int queueLowerWatermark, int queueUpperWatermark) { + sessionConnector = connector; + eventQueue = new LinkedBlockingQueue<>(); + queueTracker = newMultiSessionWatermarkTracker(eventQueue, queueLowerWatermark, queueUpperWatermark, + evt -> evt.quickfixSession); + } + + public void setExecutor(Executor executor) { + this.executor = executor; } @Override @@ -50,7 +69,7 @@ public void onMessage(Session quickfixSession, Message message) { return; } try { - eventQueue.put(new SessionMessageEvent(quickfixSession, message)); + queueTracker.put(new SessionMessageEvent(quickfixSession, message)); } catch (InterruptedException e) { isStopped = true; throw new RuntimeException(e); @@ -67,8 +86,8 @@ public void block() { synchronized (this) { if (isStopped) { if (!eventQueue.isEmpty()) { - final List tempList = new ArrayList(); - eventQueue.drainTo(tempList); + final List tempList = new ArrayList<>(); + queueTracker.drainTo(tempList); for (SessionMessageEvent event : tempList) { event.processMessage(); } @@ -90,13 +109,13 @@ public void block() { event.processMessage(); } } catch (InterruptedException e) { - // ignore + Thread.currentThread().interrupt(); } } } private SessionMessageEvent getMessage() throws InterruptedException { - return eventQueue.poll(THREAD_WAIT_FOR_MESSAGE_MS, TimeUnit.MILLISECONDS); + return queueTracker.poll(THREAD_WAIT_FOR_MESSAGE_MS, TimeUnit.MILLISECONDS); } /** @@ -109,7 +128,7 @@ private SessionMessageEvent getMessage() throws InterruptedException { */ public void blockInThread() { if (messageProcessingThread != null && messageProcessingThread.isAlive()) { - sessionConnector.log.warn("Trying to stop still running " + MESSAGE_PROCESSOR_THREAD_NAME); + sessionConnector.log.warn("Trying to stop still running {}", MESSAGE_PROCESSOR_THREAD_NAME); stopHandlingMessages(true); if (messageProcessingThread.isAlive()) { throw new IllegalStateException("Still running " + MESSAGE_PROCESSOR_THREAD_NAME + " could not be stopped!"); @@ -117,14 +136,11 @@ public void blockInThread() { } startHandlingMessages(); - messageProcessingThread = new Thread(new Runnable() { - @Override - public void run() { - sessionConnector.log.info("Started " + MESSAGE_PROCESSOR_THREAD_NAME); - block(); - sessionConnector.log.info("Stopped " + MESSAGE_PROCESSOR_THREAD_NAME); - } - }, MESSAGE_PROCESSOR_THREAD_NAME); + messageProcessingThread = new ThreadAdapter(() -> { + sessionConnector.log.info("Started {}", MESSAGE_PROCESSOR_THREAD_NAME); + block(); + sessionConnector.log.info("Stopped {}", MESSAGE_PROCESSOR_THREAD_NAME); + }, MESSAGE_PROCESSOR_THREAD_NAME, executor); messageProcessingThread.setDaemon(true); messageProcessingThread.start(); } @@ -165,7 +181,7 @@ public void stopHandlingMessages(boolean join) { try { messageProcessingThread.join(); } catch (InterruptedException e) { - sessionConnector.log.error(MESSAGE_PROCESSOR_THREAD_NAME + " interrupted."); + sessionConnector.log.error("{} interrupted.", MESSAGE_PROCESSOR_THREAD_NAME); } } } @@ -181,4 +197,96 @@ public int getQueueSize(SessionID sessionID) { return getQueueSize(); } + /** + * A stand-in for the Thread class that delegates to an Executor. + * Implements all the API required by pre-existing QFJ code. + */ + static final class ThreadAdapter { + + private final Executor executor; + private final RunnableWrapper wrapper; + + ThreadAdapter(Runnable command, String name, Executor executor) { + wrapper = new RunnableWrapper(command, name); + this.executor = executor != null ? executor : new DedicatedThreadExecutor(name); + } + + public void join() throws InterruptedException { + wrapper.join(); + } + + public void setDaemon(boolean b) { + /* No-Op. Already set for DedicatedThreadExecutor. Not relevant for externally supplied Executors. */ + } + + public boolean isAlive() { + return wrapper.isAlive(); + } + + public void start() { + executor.execute(wrapper); + } + + /** + * Provides the Thread::join and Thread::isAlive semantics on the nested Runnable. + */ + static final class RunnableWrapper implements Runnable { + + private final CountDownLatch latch = new CountDownLatch(1); + private final Runnable command; + private final String name; + + public RunnableWrapper(Runnable command, String name) { + this.command = command; + this.name = name; + } + + @Override + public void run() { + Thread currentThread = Thread.currentThread(); + String threadName = currentThread.getName(); + try { + if (!name.equals(threadName)) { + currentThread.setName(name + " (" + threadName + ")"); + } + command.run(); + } finally { + latch.countDown(); + currentThread.setName(threadName); + } + } + + public void join() throws InterruptedException { + latch.await(); + } + + public boolean isAlive() { + return latch.getCount() > 0; + } + + } + + /** + * An Executor that uses it's own dedicated Thread. + * Provides equivalent behavior to the prior non-Executor approach. + */ + static final class DedicatedThreadExecutor implements Executor { + + private final String name; + + DedicatedThreadExecutor(String name) { + this.name = name; + } + + @Override + public void execute(Runnable command) { + Thread thread = new Thread(command, name); + thread.setDaemon(true); + thread.start(); + } + + } + + } + } diff --git a/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java b/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java index 06cb4b9ea..5cdd3bbe0 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java @@ -20,41 +20,59 @@ package quickfix.mina; -import quickfix.LogUtil; -import quickfix.Message; -import quickfix.Session; -import quickfix.SessionID; +import quickfix.*; import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; import java.util.List; -import java.util.concurrent.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import static quickfix.mina.QueueTrackers.newDefaultQueueTracker; +import static quickfix.mina.QueueTrackers.newSingleSessionWatermarkTracker; /** * Processes messages in a session-specific thread. */ public class ThreadPerSessionEventHandlingStrategy implements EventHandlingStrategy { - - private final ConcurrentMap dispatchers = new ConcurrentHashMap(); + private final ConcurrentMap dispatchers = new ConcurrentHashMap<>(); private final SessionConnector sessionConnector; private final int queueCapacity; + private final int queueLowerWatermark; + private final int queueUpperWatermark; + private volatile Executor executor; public ThreadPerSessionEventHandlingStrategy(SessionConnector connector, int queueCapacity) { sessionConnector = connector; this.queueCapacity = queueCapacity; + this.queueLowerWatermark = -1; + this.queueUpperWatermark = -1; + } + + public ThreadPerSessionEventHandlingStrategy(SessionConnector connector, int queueLowerWatermark, int queueUpperWatermark) { + sessionConnector = connector; + this.queueCapacity = -1; + this.queueLowerWatermark = queueLowerWatermark; + this.queueUpperWatermark = queueUpperWatermark; } + public void setExecutor(Executor executor) { + this.executor = executor; + } + @Override public void onMessage(Session quickfixSession, Message message) { MessageDispatchingThread dispatcher = dispatchers.get(quickfixSession.getSessionID()); if (dispatcher == null) { - final MessageDispatchingThread temp = new MessageDispatchingThread(quickfixSession, queueCapacity); - dispatcher = dispatchers.putIfAbsent(quickfixSession.getSessionID(), temp); - if (dispatcher == null) { - dispatcher = temp; - } - startDispatcherThread(dispatcher); + dispatcher = dispatchers.computeIfAbsent(quickfixSession.getSessionID(), sessionID -> { + final MessageDispatchingThread newDispatcher = new MessageDispatchingThread(quickfixSession, executor); + startDispatcherThread(newDispatcher); + return newDispatcher; + }); } if (message != null) { dispatcher.enqueue(message); @@ -91,26 +109,83 @@ public void stopDispatcherThreads() { Thread.currentThread().interrupt(); } - for (final Iterator iterator = dispatchersToShutdown - .iterator(); iterator.hasNext();) { - final MessageDispatchingThread messageDispatchingThread = iterator.next(); - if (messageDispatchingThread.isStopped()) { - iterator.remove(); - } - } + dispatchersToShutdown.removeIf(MessageDispatchingThread::isStopped); } } - protected class MessageDispatchingThread extends Thread { + /** + * A stand-in for the Thread class that delegates to an Executor. + * Implements all the API required by pre-existing QFJ code. + */ + protected static abstract class ThreadAdapter implements Runnable { + + private final Executor executor; + private final String name; + + public ThreadAdapter(String name, Executor executor) { + this.name = name; + this.executor = executor != null ? executor : new DedicatedThreadExecutor(name); + } + + public void start() { + executor.execute(this); + } + + @Override + public final void run() { + Thread currentThread = Thread.currentThread(); + String threadName = currentThread.getName(); + try { + if (!name.equals(threadName)) { + currentThread.setName(name + " (" + threadName + ")"); + } + doRun(); + } finally { + currentThread.setName(threadName); + } + } + + abstract void doRun(); + + /** + * An Executor that uses it's own dedicated Thread. + * Provides equivalent behavior to the prior non-Executor approach. + */ + static final class DedicatedThreadExecutor implements Executor { + + private final String name; + + DedicatedThreadExecutor(String name) { + this.name = name; + } + + @Override + public void execute(Runnable command) { + new Thread(command, name).start(); + } + + } + + } + + protected class MessageDispatchingThread extends ThreadAdapter { private final Session quickfixSession; private final BlockingQueue messages; + private final QueueTracker queueTracker; private volatile boolean stopped; private volatile boolean stopping; - private MessageDispatchingThread(Session session, int queueCapacity) { - super("QF/J Session dispatcher: " + session.getSessionID()); + private MessageDispatchingThread(Session session, Executor executor) { + super("QF/J Session dispatcher: " + session.getSessionID(), executor); quickfixSession = session; - messages = new LinkedBlockingQueue(queueCapacity); + if (queueCapacity >= 0) { + messages = new LinkedBlockingQueue<>(queueCapacity); + queueTracker = newDefaultQueueTracker(messages); + } else { + messages = new LinkedBlockingQueue<>(); + queueTracker = newSingleSessionWatermarkTracker(messages, queueLowerWatermark, queueUpperWatermark, + quickfixSession); + } } public void enqueue(Message message) { @@ -118,7 +193,7 @@ public void enqueue(Message message) { return; } try { - messages.put(message); + queueTracker.put(message); } catch (final InterruptedException e) { quickfixSession.getLog().onErrorEvent(e.toString()); } @@ -129,10 +204,10 @@ public int getQueueSize() { } @Override - public void run() { + void doRun() { while (!stopping) { try { - final Message message = getNextMessage(messages); + final Message message = getNextMessage(queueTracker); if (message == null) { // no message available in polling interval continue; @@ -151,8 +226,8 @@ public void run() { } } if (!messages.isEmpty()) { - final List tempList = new ArrayList(); - messages.drainTo(tempList); + final List tempList = new ArrayList<>(); + queueTracker.drainTo(tempList); for (Message message : tempList) { try { quickfixSession.next(message); @@ -188,12 +263,12 @@ protected MessageDispatchingThread getDispatcher(SessionID sessionID) { * We do not block indefinitely as that would prevent this thread from ever stopping * * @see #THREAD_WAIT_FOR_MESSAGE_MS - * @param messages + * @param queueTracker * @return next message or null if nothing arrived within the timeout period * @throws InterruptedException */ - protected Message getNextMessage(BlockingQueue messages) throws InterruptedException { - return messages.poll(THREAD_WAIT_FOR_MESSAGE_MS, TimeUnit.MILLISECONDS); + protected Message getNextMessage(QueueTracker queueTracker) throws InterruptedException { + return queueTracker.poll(THREAD_WAIT_FOR_MESSAGE_MS, TimeUnit.MILLISECONDS); } @Override diff --git a/quickfixj-core/src/main/java/quickfix/mina/WatermarkTracker.java b/quickfixj-core/src/main/java/quickfix/mina/WatermarkTracker.java new file mode 100644 index 000000000..25e910e7b --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/mina/WatermarkTracker.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * 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.mina; + +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * A blocking queue wrapper implementing watermarks-based back pressure propagation + * from the queue sink to one or more logical sources. + * + * @param payload type + * @param logical source key type + * + * @author Vladimir Lysyy (mrbald@github) + */ +public class WatermarkTracker implements QueueTracker { + private final BlockingQueue queue; + private final long lowerWatermark; + private final long upperWatermark; + private final Consumer onLowerWatermarkCrossed; + private final Consumer onUpperWatermarkCrossed; + private final Function classifier; + private final Function trackerSupplier; + + class StreamTracker { + private final S key; + long counter = 0; + private boolean suspended = false; + + StreamTracker(S key) { + this.key = key; + } + + synchronized void incoming(int n) { + if ((counter += n) >= upperWatermark && !suspended) { + suspended = true; + onUpperWatermarkCrossed.accept(key); + } + } + + synchronized void outgoing(int n) { + if ((counter -= n) == lowerWatermark && suspended) { + suspended = false; + onLowerWatermarkCrossed.accept(key); + } + } + + synchronized boolean isSuspended() { + return suspended; + } + } + + static WatermarkTracker newMono( + BlockingQueue queue, + long lowerWatermark, long upperWatermark, + Runnable onLowerWatermarkCrossed, Runnable onUpperWatermarkCrossed) { + return new WatermarkTracker<>(queue, lowerWatermark, upperWatermark, onLowerWatermarkCrossed, onUpperWatermarkCrossed); + } + + static WatermarkTracker newMulti( + BlockingQueue queue, + long lowerWatermark, long upperWatermark, + Function classifier, + Consumer onLowerWatermarkCrossed, Consumer onUpperWatermarkCrossed) { + return new WatermarkTracker<>(queue, lowerWatermark, upperWatermark, classifier, onLowerWatermarkCrossed, onUpperWatermarkCrossed); + } + + private WatermarkTracker( + BlockingQueue queue, + long lowerWatermark, long upperWatermark, + Function classifier, + Consumer onLowerWatermarkCrossed, Consumer onUpperWatermarkCrossed) { + this.queue = queue; + this.lowerWatermark = lowerWatermark; + this.upperWatermark = upperWatermark; + this.classifier = classifier; + this.onLowerWatermarkCrossed = onLowerWatermarkCrossed; + this.onUpperWatermarkCrossed = onUpperWatermarkCrossed; + + final Map trackerMap = new ConcurrentHashMap<>(); + + this.trackerSupplier = key -> trackerMap.computeIfAbsent(key, StreamTracker::new); + } + + private WatermarkTracker( + BlockingQueue queue, + long lowerWatermark, long upperWatermark, + Runnable onLowerWatermarkCrossed, Runnable onUpperWatermarkCrossed) { + this.queue = queue; + this.lowerWatermark = lowerWatermark; + this.upperWatermark = upperWatermark; + this.classifier = x -> null; + this.onLowerWatermarkCrossed = x -> onLowerWatermarkCrossed.run(); + this.onUpperWatermarkCrossed = x -> onUpperWatermarkCrossed.run(); + + final StreamTracker streamTracker = new StreamTracker(null); + + this.trackerSupplier = key -> streamTracker; + } + + @Override + public void put(E e) throws InterruptedException { + queue.put(e); + trackerForPayload(e).incoming(1); + } + + @Override + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + final E e = queue.poll(timeout, unit); + + if (e != null) { + trackerForPayload(e).outgoing(1); + } + + return e; + } + + @Override + public int drainTo(Collection collection) { + return queue.drainTo(new AbstractCollection() { + @Override public Iterator iterator() { throw new UnsupportedOperationException(); } + @Override public int size() { throw new UnsupportedOperationException(); } + + public boolean add(E e) { + final boolean added = collection.add(e); + if (added) { + trackerForPayload(e).outgoing(1); + } + return added; + } + + }); + } + + public boolean isSuspended(S key) { + return trackerForStream(key).isSuspended(); + } + + public boolean isSuspended() { + return isSuspended(null); + } + + StreamTracker trackerForPayload(E e) { + return trackerForStream(classifier.apply(e)); + } + + StreamTracker trackerForStream(S s) { + return trackerSupplier.apply(s); + } + +} 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 4cd799504..c78e19fe3 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java @@ -19,21 +19,10 @@ package quickfix.mina.acceptor; -import java.net.SocketAddress; -import java.security.GeneralSecurityException; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import javax.net.ssl.SSLContext; - import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.buffer.SimpleBufferAllocator; import org.apache.mina.core.service.IoAcceptor; import org.apache.mina.filter.codec.ProtocolCodecFilter; - import quickfix.Acceptor; import quickfix.Application; import quickfix.ConfigError; @@ -59,14 +48,23 @@ import quickfix.mina.ssl.SSLFilter; import quickfix.mina.ssl.SSLSupport; +import javax.net.ssl.SSLContext; +import java.net.SocketAddress; +import java.security.GeneralSecurityException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + /** * Abstract base class for socket acceptors. */ public abstract class AbstractSocketAcceptor extends SessionConnector implements Acceptor { - private final Map sessionProviders = new HashMap(); + private final Map sessionProviders = new HashMap<>(); private final SessionFactory sessionFactory; - private final Map socketDescriptorForAddress = new HashMap(); - private final Map ioAcceptors = new HashMap(); + private final Map socketDescriptorForAddress = new HashMap<>(); + private final Map ioAcceptors = new HashMap<>(); protected AbstractSocketAcceptor(SessionSettings settings, SessionFactory sessionFactory) throws ConfigError { @@ -113,20 +111,19 @@ protected synchronized void startAcceptingConnections() throws ConfigError { ioAcceptor.setFilterChainBuilder(ioFilterChainBuilder); ioAcceptor.setCloseOnDeactivation(false); ioAcceptor.bind(socketDescriptor.getAddress()); - log.info("Listening for connections at " + address + " for session(s) " - + socketDescriptor.getAcceptedSessions().keySet()); + log.info("Listening for connections at {} for session(s) {}", address, socketDescriptor.getAcceptedSessions().keySet()); } } catch (FieldConvertError e) { throw new ConfigError(e); } catch (Exception e) { - log.error("Cannot start acceptor session for " + address + ", error:" + e); + log.error("Cannot start acceptor session for {}, error: {}", address, e); throw new RuntimeError(e); } } private void installSSL(AcceptorSocketDescriptor descriptor, CompositeIoFilterChainBuilder ioFilterChainBuilder) throws GeneralSecurityException { - log.info("Installing SSL filter for " + descriptor.getAddress()); + log.info("Installing SSL filter for {}", descriptor.getAddress()); SSLConfig sslConfig = descriptor.getSslConfig(); SSLContext sslContext = SSLContextFactory.getInstance(sslConfig); SSLFilter sslFilter = new SSLFilter(sslContext); @@ -141,11 +138,9 @@ private void installSSL(AcceptorSocketDescriptor descriptor, private IoAcceptor getIoAcceptor(AcceptorSocketDescriptor socketDescriptor, boolean init) throws ConfigError { int transportType = ProtocolFactory.getAddressTransportType(socketDescriptor.getAddress()); - AcceptorSessionProvider sessionProvider = sessionProviders.get(socketDescriptor.getAddress()); - if (sessionProvider == null) { - sessionProvider = new DefaultAcceptorSessionProvider(socketDescriptor.getAcceptedSessions()); - sessionProviders.put(socketDescriptor.getAddress(), sessionProvider); - } + AcceptorSessionProvider sessionProvider = sessionProviders. + computeIfAbsent(socketDescriptor.getAddress(), + k -> new DefaultAcceptorSessionProvider(socketDescriptor.getAcceptedSessions())); IoAcceptor ioAcceptor = ioAcceptors.get(socketDescriptor); if (ioAcceptor == null && init) { @@ -187,8 +182,7 @@ && getSettings().getBool(sessionID, SSLSupport.SETTING_USE_SSL)) { useSSL = true; sslConfig = SSLSupport.getSslConfig(getSettings(), sessionID); } else { - log.warn("SSL will not be enabled for transport type=" + acceptTransportType - + ", session=" + sessionID); + log.warn("SSL will not be enabled for transport type={}, session={}", acceptTransportType, sessionID); } } @@ -221,7 +215,7 @@ private boolean equals(Object object1, Object object2) { } private void createSessions(SessionSettings settings) throws ConfigError, FieldConvertError { - HashMap allSessions = new HashMap(); + HashMap allSessions = new HashMap<>(); for (Iterator i = settings.sectionIterator(); i.hasNext();) { SessionID sessionID = i.next(); String connectionType = settings.getString(sessionID, @@ -248,15 +242,14 @@ private void createSessions(SessionSettings settings) throws ConfigError, FieldC } } - // XXX does this need to by synchronized? protected void stopAcceptingConnections() throws ConfigError { Iterator ioIt = getEndpoints().iterator(); while (ioIt.hasNext()) { IoAcceptor ioAcceptor = ioIt.next(); SocketAddress localAddress = ioAcceptor.getLocalAddress(); ioAcceptor.unbind(); - ioAcceptor.dispose(true); - log.info("No longer accepting connections on " + localAddress); + closeManagedSessionsAndDispose(ioAcceptor, true, log); + log.info("No longer accepting connections on {}", localAddress); ioIt.remove(); } } @@ -265,7 +258,7 @@ private static class AcceptorSocketDescriptor { private final SocketAddress address; private final boolean useSSL; private final SSLConfig sslConfig; - private final Map acceptedSessions = new HashMap(); + private final Map acceptedSessions = new HashMap<>(); public AcceptorSocketDescriptor(SocketAddress address, boolean useSSL, SSLConfig sslConfig) { this.address = address; @@ -299,7 +292,7 @@ public Collection getEndpoints() { } public Map getAcceptorAddresses() { - Map sessionIdToAddressMap = new HashMap(); + Map sessionIdToAddressMap = new HashMap<>(); for (AcceptorSocketDescriptor descriptor : socketDescriptorForAddress.values()) { for (SessionID sessionID : descriptor.getAcceptedSessions().keySet()) { sessionIdToAddressMap.put(sessionID, descriptor.getAddress()); 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 37921aafe..4ca42ef04 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AcceptorIoHandler.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AcceptorIoHandler.java @@ -42,7 +42,6 @@ class AcceptorIoHandler extends AbstractIoHandler { private final EventHandlingStrategy eventHandlingStrategy; - private final AcceptorSessionProvider sessionProvider; public AcceptorIoHandler(AcceptorSessionProvider sessionProvider, @@ -55,8 +54,7 @@ public AcceptorIoHandler(AcceptorSessionProvider sessionProvider, @Override public void sessionCreated(IoSession session) throws Exception { super.sessionCreated(session); - log.info("MINA session created: " + "local=" + session.getLocalAddress() + ", " - + session.getClass() + ", remote=" + session.getRemoteAddress()); + log.info("MINA session created: local={}, {}, remote={}", session.getLocalAddress(), session.getClass(), session.getRemoteAddress()); } @Override @@ -77,7 +75,7 @@ protected void processMessage(IoSession protocolSession, Message message) throws } sessionLog.onEvent("Accepting session " + qfSession.getSessionID() + " from " + protocolSession.getRemoteAddress()); - final int heartbeatInterval = message.getInt(HeartBtInt.FIELD); + final int heartbeatInterval = message.isSetField(HeartBtInt.FIELD) ? message.getInt(HeartBtInt.FIELD) : 0; qfSession.setHeartBeatInterval(heartbeatInterval); sessionLog.onEvent("Acceptor heartbeat set to " + heartbeatInterval + " seconds"); @@ -91,20 +89,20 @@ protected void processMessage(IoSession protocolSession, Message message) throws final ApplVerID applVerID = new ApplVerID( message.getString(DefaultApplVerID.FIELD)); qfSession.setTargetDefaultApplicationVersionID(applVerID); - log.info("Setting DefaultApplVerID (" + DefaultApplVerID.FIELD + "=" + sessionLog.onEvent("Setting DefaultApplVerID (" + DefaultApplVerID.FIELD + "=" + applVerID.getValue() + ") from Logon"); } } } else { - log.error("Unknown session ID during logon: " + sessionID - + " cannot be found in session list " - + eventHandlingStrategy.getSessionConnector().getSessions() - + " (connecting from " + protocolSession.getRemoteAddress() + " to " - + protocolSession.getLocalAddress() + ")"); + log.error("Unknown session ID during logon: {} cannot be found in session list {} (connecting from {} to {})", + sessionID, + eventHandlingStrategy.getSessionConnector().getSessions(), + protocolSession.getRemoteAddress(), + protocolSession.getLocalAddress()); return; } } else { - log.warn("Ignoring non-logon message before session establishment: " + message); + log.warn("Ignoring non-logon message before session establishment: {}", message); protocolSession.closeNow(); return; } 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 f5f0f5933..b17ca5838 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java @@ -19,20 +19,10 @@ package quickfix.mina.initiator; -import java.net.SocketAddress; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.buffer.SimpleBufferAllocator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import quickfix.Application; import quickfix.ConfigError; import quickfix.DefaultSessionFactory; @@ -53,13 +43,22 @@ import quickfix.mina.ssl.SSLConfig; import quickfix.mina.ssl.SSLSupport; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + /** * Abstract base class for socket initiators. */ public abstract class AbstractSocketInitiator extends SessionConnector implements Initiator { protected final Logger log = LoggerFactory.getLogger(getClass()); - private final Set initiators = new HashSet(); + private final Set initiators = new HashSet<>(); protected AbstractSocketInitiator(Application application, MessageStoreFactory messageStoreFactory, SessionSettings settings, @@ -78,9 +77,6 @@ protected AbstractSocketInitiator(SessionSettings settings, SessionFactory sessi protected void createSessionInitiators() throws ConfigError { try { - // QFJ698: clear() is needed on restart, otherwise the set gets filled up with - // more and more initiators which are not equal because the local port differs - initiators.clear(); createSessions(); SessionSettings settings = getSettings(); for (final Session session : getSessionMap().values()) { @@ -105,10 +101,46 @@ protected void createSessionInitiators() sslConfig = SSLSupport.getSslConfig(getSettings(), sessionID); } + String proxyUser = null; + String proxyPassword = null; + String proxyHost = null; + + String proxyType = null; + String proxyVersion = null; + + String proxyWorkstation = null; + String proxyDomain = null; + + int proxyPort = -1; + + if (getSettings().isSetting(sessionID, Initiator.SETTING_PROXY_TYPE)) { + proxyType = settings.getString(sessionID, Initiator.SETTING_PROXY_TYPE); + if (getSettings().isSetting(sessionID, Initiator.SETTING_PROXY_VERSION)) { + proxyVersion = settings.getString(sessionID, + Initiator.SETTING_PROXY_VERSION); + } + + if (getSettings().isSetting(sessionID, Initiator.SETTING_PROXY_USER)) { + proxyUser = settings.getString(sessionID, Initiator.SETTING_PROXY_USER); + proxyPassword = settings.getString(sessionID, + Initiator.SETTING_PROXY_PASSWORD); + } + if (getSettings().isSetting(sessionID, Initiator.SETTING_PROXY_WORKSTATION) + && getSettings().isSetting(sessionID, Initiator.SETTING_PROXY_DOMAIN)) { + proxyWorkstation = settings.getString(sessionID, + Initiator.SETTING_PROXY_WORKSTATION); + proxyDomain = settings.getString(sessionID, Initiator.SETTING_PROXY_DOMAIN); + } + + proxyHost = settings.getString(sessionID, Initiator.SETTING_PROXY_HOST); + proxyPort = (int) settings.getLong(sessionID, Initiator.SETTING_PROXY_PORT); + } + final IoSessionInitiator ioSessionInitiator = new IoSessionInitiator(session, - socketAddresses, localAddress, reconnectingIntervals, getScheduledExecutorService(), - networkingOptions, getEventHandlingStrategy(), getIoFilterChainBuilder(), - sslEnabled, sslConfig); + socketAddresses, localAddress, reconnectingIntervals, + getScheduledExecutorService(), networkingOptions, + getEventHandlingStrategy(), getIoFilterChainBuilder(), sslEnabled, sslConfig, + proxyType, proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, proxyWorkstation); initiators.add(ioSessionInitiator); } @@ -132,9 +164,7 @@ private SocketAddress getLocalAddress(SessionSettings settings, final SessionID port = (int) settings.getLong(sessionID, Initiator.SETTING_SOCKET_LOCAL_PORT); } localAddress = ProtocolFactory.createSocketAddress(ProtocolFactory.SOCKET, host, port); - if (log.isInfoEnabled()) { - log.info("Using initiator local host: " + localAddress); - } + log.info("Using initiator local host: {}", localAddress); } return localAddress; } @@ -146,7 +176,7 @@ private void createSessions() throws ConfigError, FieldConvertError { continueInitOnError = settings.getBool(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR); } - final Map initiatorSessions = new HashMap(); + final Map initiatorSessions = new HashMap<>(); for (final Iterator i = settings.sectionIterator(); i.hasNext();) { final SessionID sessionID = i.next(); if (isInitiatorSession(sessionID)) { @@ -188,7 +218,7 @@ private int[] getReconnectIntervalInSeconds(SessionID sessionID) throws ConfigEr private SocketAddress[] getSocketAddresses(SessionID sessionID) throws ConfigError { final SessionSettings settings = getSettings(); - final ArrayList addresses = new ArrayList(); + final ArrayList addresses = new ArrayList<>(); for (int index = 0;; index++) { try { final String protocolKey = Initiator.SETTING_SOCKET_CONNECT_PROTOCOL @@ -219,7 +249,7 @@ private SocketAddress[] getSocketAddresses(SessionID sessionID) throws ConfigErr break; } } catch (final FieldConvertError e) { - throw (ConfigError) new ConfigError(e.getMessage()).initCause(e); + throw new ConfigError(e.getMessage(), e); } } @@ -245,8 +275,9 @@ protected void startInitiators() { } protected void stopInitiators() { - for (final IoSessionInitiator initiator : initiators) { - initiator.stop(); + for (Iterator iterator = initiators.iterator(); iterator.hasNext();) { + iterator.next().stop(); + iterator.remove(); } super.stopSessionTimer(); } diff --git a/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorIoHandler.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorIoHandler.java index 1b40c2d25..dd1033b18 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorIoHandler.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorIoHandler.java @@ -54,7 +54,7 @@ public void sessionCreated(IoSession session) throws Exception { networkingOptions.getSynchronousWrites(), networkingOptions.getSynchronousWriteTimeout(), quickfixSession.getMaxScheduledWriteRequests())); - log.info("MINA session created for " + quickfixSession.getSessionID() + ": local=" + quickfixSession.getLog().onEvent("MINA session created: local=" + session.getLocalAddress() + ", " + session.getClass() + ", remote=" + session.getRemoteAddress()); } @@ -67,7 +67,7 @@ protected void processMessage(IoSession protocolSession, Message message) throws if (message.isSetField(DefaultApplVerID.FIELD)) { final ApplVerID applVerID = new ApplVerID(message.getString(DefaultApplVerID.FIELD)); quickfixSession.setTargetDefaultApplicationVersionID(applVerID); - log.info("Setting DefaultApplVerID (" + DefaultApplVerID.FIELD + "=" + quickfixSession.getLog().onEvent("Setting DefaultApplVerID (" + DefaultApplVerID.FIELD + "=" + applVerID.getValue() + ") from Logon"); } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorProxyIoHandler.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorProxyIoHandler.java new file mode 100644 index 000000000..49bbdf620 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorProxyIoHandler.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * 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.mina.initiator; + +import org.apache.mina.core.session.IoSession; +import org.apache.mina.proxy.AbstractProxyIoHandler; + +import quickfix.mina.ssl.SSLFilter; + +class InitiatorProxyIoHandler extends AbstractProxyIoHandler { + private final InitiatorIoHandler initiatorIoHandler; + private final SSLFilter sslFilter; + + InitiatorProxyIoHandler(InitiatorIoHandler initiatorIoHandler, SSLFilter sslFilter) { + super(); + this.initiatorIoHandler = initiatorIoHandler; + this.sslFilter = sslFilter; + } + + @Override + public void sessionCreated(IoSession session) throws Exception { + this.initiatorIoHandler.sessionCreated(session); + } + + @Override + public void sessionClosed(IoSession ioSession) throws Exception { + this.initiatorIoHandler.sessionClosed(ioSession); + } + + @Override + public void messageReceived(IoSession session, Object message) throws Exception { + this.initiatorIoHandler.messageReceived(session, message); + } + + @Override + public void messageSent(IoSession session, Object message) throws Exception { + this.initiatorIoHandler.messageSent(session, message); + } + + @Override + public void exceptionCaught(IoSession ioSession, Throwable cause) throws Exception { + this.initiatorIoHandler.exceptionCaught(ioSession, cause); + } + + @Override + public void proxySessionOpened(IoSession ioSession) throws Exception { + if (this.sslFilter != null) { + this.sslFilter.initiateHandshake(ioSession); + } + } +} 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 bf02ed963..e70e6325d 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java @@ -24,8 +24,8 @@ import org.apache.mina.core.service.IoConnector; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.mina.proxy.ProxyConnector; +import org.apache.mina.transport.socket.SocketConnector; import quickfix.ConfigError; import quickfix.LogUtil; import quickfix.Session; @@ -34,6 +34,7 @@ import quickfix.mina.EventHandlingStrategy; import quickfix.mina.NetworkingOptions; import quickfix.mina.ProtocolFactory; +import quickfix.mina.SessionConnector; import quickfix.mina.message.FIXProtocolCodecFactory; import quickfix.mina.ssl.SSLConfig; import quickfix.mina.ssl.SSLContextFactory; @@ -49,32 +50,39 @@ import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IoSessionInitiator { private final static long CONNECT_POLL_TIMEOUT = 2000L; private final ScheduledExecutorService executor; private final ConnectTask reconnectTask; + private final Logger log = LoggerFactory.getLogger(getClass()); private Future reconnectFuture; - protected final static Logger log = LoggerFactory.getLogger("display." + IoSessionInitiator.class.getName()); - public IoSessionInitiator(Session fixSession, SocketAddress[] socketAddresses, SocketAddress localAddress, - int[] reconnectIntervalInSeconds, ScheduledExecutorService executor, - NetworkingOptions networkingOptions, EventHandlingStrategy eventHandlingStrategy, - IoFilterChainBuilder userIoFilterChainBuilder, boolean sslEnabled, SSLConfig sslConfig) throws ConfigError { + public IoSessionInitiator(Session fixSession, SocketAddress[] socketAddresses, + SocketAddress localAddress, int[] reconnectIntervalInSeconds, + ScheduledExecutorService executor, NetworkingOptions networkingOptions, + EventHandlingStrategy eventHandlingStrategy, + IoFilterChainBuilder userIoFilterChainBuilder, boolean sslEnabled, SSLConfig sslConfig, + String proxyType, String proxyVersion, String proxyHost, int proxyPort, + String proxyUser, String proxyPassword, String proxyDomain, String proxyWorkstation) throws ConfigError { this.executor = executor; final long[] reconnectIntervalInMillis = new long[reconnectIntervalInSeconds.length]; for (int ii = 0; ii != reconnectIntervalInSeconds.length; ++ii) { reconnectIntervalInMillis[ii] = reconnectIntervalInSeconds[ii] * 1000L; } try { - reconnectTask = new ConnectTask(sslEnabled, socketAddresses, localAddress, userIoFilterChainBuilder, - fixSession, reconnectIntervalInMillis, networkingOptions, - eventHandlingStrategy, sslConfig); + reconnectTask = new ConnectTask(sslEnabled, socketAddresses, localAddress, + userIoFilterChainBuilder, fixSession, reconnectIntervalInMillis, + networkingOptions, eventHandlingStrategy, sslConfig, + proxyType, proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, proxyWorkstation, log); } catch (GeneralSecurityException e) { throw new ConfigError(e); } - log.info("[" + fixSession.getSessionID() + "] " + Arrays.asList(socketAddresses)); + + fixSession.getLog().onEvent("Configured socket addresses for session: " + Arrays.asList(socketAddresses)); } private static class ConnectTask implements Runnable { @@ -88,6 +96,7 @@ private static class ConnectTask implements Runnable { private final NetworkingOptions networkingOptions; private final EventHandlingStrategy eventHandlingStrategy; private final SSLConfig sslConfig; + private final Logger log; private IoSession ioSession; private long lastReconnectAttemptTime; @@ -96,10 +105,22 @@ private static class ConnectTask implements Runnable { private int connectionFailureCount; private ConnectFuture connectFuture; + private final String proxyType; + private final String proxyVersion; + private final String proxyHost; + private final int proxyPort; + private final String proxyUser; + private final String proxyPassword; + private final String proxyDomain; + private final String proxyWorkstation; + public ConnectTask(boolean sslEnabled, SocketAddress[] socketAddresses, - SocketAddress localAddress, IoFilterChainBuilder userIoFilterChainBuilder, Session fixSession, - long[] reconnectIntervalInMillis, NetworkingOptions networkingOptions, - EventHandlingStrategy eventHandlingStrategy, SSLConfig sslConfig) throws ConfigError, GeneralSecurityException { + SocketAddress localAddress, IoFilterChainBuilder userIoFilterChainBuilder, + Session fixSession, long[] reconnectIntervalInMillis, + NetworkingOptions networkingOptions, EventHandlingStrategy eventHandlingStrategy, SSLConfig sslConfig, + String proxyType, String proxyVersion, String proxyHost, + int proxyPort, String proxyUser, String proxyPassword, String proxyDomain, + String proxyWorkstation, Logger log) throws ConfigError, GeneralSecurityException { this.sslEnabled = sslEnabled; this.socketAddresses = socketAddresses; this.localAddress = localAddress; @@ -109,37 +130,70 @@ public ConnectTask(boolean sslEnabled, SocketAddress[] socketAddresses, this.networkingOptions = networkingOptions; this.eventHandlingStrategy = eventHandlingStrategy; this.sslConfig = sslConfig; + this.log = log; + + this.proxyType = proxyType; + this.proxyVersion = proxyVersion; + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.proxyUser = proxyUser; + this.proxyPassword = proxyPassword; + this.proxyDomain = proxyDomain; + this.proxyWorkstation = proxyWorkstation; + setupIoConnector(); } private void setupIoConnector() throws ConfigError, GeneralSecurityException { - final IoConnector newConnector = ProtocolFactory.createIoConnector(socketAddresses[0]); final CompositeIoFilterChainBuilder ioFilterChainBuilder = new CompositeIoFilterChainBuilder(userIoFilterChainBuilder); + boolean hasProxy = proxyType != null && proxyPort > 0 && socketAddresses[nextSocketAddressIndex] instanceof InetSocketAddress; + + SSLFilter sslFilter = null; if (sslEnabled) { - installSslFilter(ioFilterChainBuilder); + sslFilter = installSslFilter(ioFilterChainBuilder, !hasProxy); } ioFilterChainBuilder.addLast(FIXProtocolCodecFactory.FILTER_NAME, new ProtocolCodecFilter(new FIXProtocolCodecFactory())); - newConnector.setFilterChainBuilder(ioFilterChainBuilder); + IoConnector newConnector; + newConnector = ProtocolFactory.createIoConnector(socketAddresses[nextSocketAddressIndex]); newConnector.setHandler(new InitiatorIoHandler(fixSession, networkingOptions, eventHandlingStrategy)); + newConnector.setFilterChainBuilder(ioFilterChainBuilder); + + if (hasProxy) { + ProxyConnector proxyConnector = ProtocolFactory.createIoProxyConnector( + (SocketConnector) newConnector, + (InetSocketAddress) socketAddresses[nextSocketAddressIndex], + new InetSocketAddress(proxyHost, proxyPort), + proxyType, proxyVersion, proxyUser, proxyPassword, proxyDomain, proxyWorkstation + ); + + proxyConnector.setHandler(new InitiatorProxyIoHandler( + new InitiatorIoHandler(fixSession, networkingOptions, eventHandlingStrategy), + sslFilter + )); + + newConnector = proxyConnector; + } + if (ioConnector != null) { - ioConnector.dispose(); + SessionConnector.closeManagedSessionsAndDispose(ioConnector, true, log); } ioConnector = newConnector; } - private void installSslFilter(CompositeIoFilterChainBuilder ioFilterChainBuilder) + private SSLFilter installSslFilter(CompositeIoFilterChainBuilder ioFilterChainBuilder, boolean autoStart) throws GeneralSecurityException { final SSLContext sslContext = SSLContextFactory.getInstance(sslConfig); - final SSLFilter sslFilter = new SSLFilter(sslContext); + final SSLFilter sslFilter = new SSLFilter(sslContext, autoStart); sslFilter.setUseClientMode(true); sslFilter.setCipherSuites(sslConfig.getEnabledCipherSuites() != null ? sslConfig.getEnabledCipherSuites() : SSLSupport.getDefaultCipherSuites(sslContext)); sslFilter.setEnabledProtocols(sslConfig.getEnabledProtocols() != null ? sslConfig.getEnabledProtocols() : SSLSupport.getSupportedProtocols(sslContext)); ioFilterChainBuilder.addLast(SSLSupport.FILTER_NAME, sslFilter); + return sslFilter; } public synchronized void run() { @@ -179,6 +233,7 @@ private void pollConnectFuture() { if (connectFuture.getSession() != null) { ioSession = connectFuture.getSession(); connectionFailureCount = 0; + nextSocketAddressIndex = 0; lastConnectTime = System.currentTimeMillis(); connectFuture = null; } else { @@ -281,17 +336,19 @@ public Session getFixSession() { } private void resetIoConnector() { - if (ioSession != null && Boolean.TRUE.equals(ioSession.getAttribute("QFJ_RESET_IO_CONNECTOR"))) { + if (ioSession != null && Boolean.TRUE.equals(ioSession.getAttribute(SessionConnector.QFJ_RESET_IO_CONNECTOR))) { try { setupIoConnector(); - log.info("[" + fixSession.getSessionID() + "] - reset IoConnector"); if (connectFuture != null) { connectFuture.cancel(); } connectFuture = null; + if (!ioSession.isClosing()) { + ioSession.closeNow(); + } ioSession = null; } catch (Throwable e) { - log.error("[" + fixSession.getSessionID() + "] - Exception during resetIoConnector call", e); + LogUtil.logThrowable(fixSession.getLog(), "Exception during resetIoConnector call", e); } } } @@ -312,7 +369,6 @@ synchronized void stop() { reconnectFuture.cancel(true); reconnectFuture = null; } - // QFJ-849: clean up resources of MINA connector - reconnectTask.ioConnector.dispose(); + SessionConnector.closeManagedSessionsAndDispose(reconnectTask.ioConnector, true, log); } } 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 b7aa92356..9f6b6057a 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/message/FIXMessageDecoder.java +++ b/quickfixj-core/src/main/java/quickfix/mina/message/FIXMessageDecoder.java @@ -19,18 +19,9 @@ package quickfix.mina.message; -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.io.UnsupportedEncodingException; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.List; - import org.apache.mina.core.buffer.IoBuffer; -import org.apache.mina.core.session.IoSession; import org.apache.mina.core.filterchain.IoFilter; +import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecException; import org.apache.mina.filter.codec.ProtocolDecoderOutput; import org.apache.mina.filter.codec.demux.MessageDecoder; @@ -38,9 +29,17 @@ import org.quickfixj.CharsetSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import quickfix.mina.CriticalProtocolCodecException; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.List; + /** * Detects and decodes FIX message strings in an incoming data stream. The * message string is then passed to MINA IO handlers for further processing. @@ -140,7 +139,7 @@ private boolean parseMessage(IoBuffer in, ProtocolDecoderOutput out) in.position(headerOffset); if (log.isDebugEnabled()) { - log.debug("detected header: " + getBufferDebugInfo(in)); + log.debug("detected header: {}", getBufferDebugInfo(in)); } position = headerOffset + headerLength; @@ -166,7 +165,7 @@ private boolean parseMessage(IoBuffer in, ProtocolDecoderOutput out) if (ch == SOH) { state = READING_BODY; if (log.isDebugEnabled()) { - log.debug("body length = " + bodyLength + ": " + getBufferDebugInfo(in)); + log.debug("body length = {}: {}", bodyLength, getBufferDebugInfo(in)); } } else { if (position < in.limit()) { // if data remains @@ -187,7 +186,7 @@ private boolean parseMessage(IoBuffer in, ProtocolDecoderOutput out) position += bodyLength; state = PARSING_CHECKSUM; if (log.isDebugEnabled()) { - log.debug("message body found: " + getBufferDebugInfo(in)); + log.debug("message body found: {}", getBufferDebugInfo(in)); } } @@ -202,7 +201,7 @@ private boolean parseMessage(IoBuffer in, ProtocolDecoderOutput out) continue; } if (log.isDebugEnabled()) { - log.debug("found checksum: " + getBufferDebugInfo(in)); + log.debug("found checksum: {}", getBufferDebugInfo(in)); } position += CHECKSUM_PATTERN.getMinLength(); } else { @@ -221,7 +220,7 @@ private boolean parseMessage(IoBuffer in, ProtocolDecoderOutput out) } String messageString = getMessageString(in); if (log.isDebugEnabled()) { - log.debug("parsed message: " + getBufferDebugInfo(in) + " " + messageString); + log.debug("parsed message: {} {}", getBufferDebugInfo(in), messageString); } out.write(messageString); // eventually invokes AbstractIoHandler.messageReceived state = SEEKING_HEADER; @@ -302,13 +301,8 @@ public interface MessageListener { * quickfix.mina.message.FIXMessageDecoder.MessageListener) */ public List extractMessages(File file) throws IOException, ProtocolCodecException { - final List messages = new ArrayList(); - extractMessages(file, new MessageListener() { - @Override - public void onMessage(String message) { - messages.add(message); - } - }); + final List messages = new ArrayList<>(); + extractMessages(file, messages::add); return messages; } @@ -326,8 +320,7 @@ public void onMessage(String message) { public void extractMessages(File file, final MessageListener listener) throws IOException, ProtocolCodecException { // Set up a read-only memory-mapped file - RandomAccessFile fileIn = new RandomAccessFile(file, "r"); - try { + try (RandomAccessFile fileIn = new RandomAccessFile(file, "r")) { FileChannel readOnlyChannel = fileIn.getChannel(); MappedByteBuffer memoryMappedBuffer = readOnlyChannel.map(FileChannel.MapMode.READ_ONLY, 0, (int) readOnlyChannel.size()); @@ -336,13 +329,12 @@ public void extractMessages(File file, final MessageListener listener) throws IO public void write(Object message) { listener.onMessage((String) message); } + @Override public void flush(IoFilter.NextFilter nextFilter, IoSession ioSession) { // ignored } }); - } finally { - fileIn.close(); } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextFactory.java b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextFactory.java index 53d3aca48..513f35b15 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextFactory.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextFactory.java @@ -19,6 +19,15 @@ package quickfix.mina.ssl; +import org.apache.mina.filter.ssl.BogusTrustManagerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quickfix.FileUtil; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; @@ -30,17 +39,6 @@ import java.util.HashMap; import java.util.Map; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; - -import org.apache.mina.filter.ssl.BogusTrustManagerFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import quickfix.FileUtil; - /** * SSL context factory that deals with various SSL configuration. * Caches the created SSL contexts for future reuse. @@ -48,7 +46,7 @@ public class SSLContextFactory { private static final Logger log = LoggerFactory.getLogger(SSLContextFactory.class); private static final String PROTOCOL = "TLS"; - private static final Map contextCache = new HashMap(); + private static final Map contextCache = new HashMap<>(); /** * Creates an {@link SSLContext} with a specified {@link SSLConfig} @@ -110,7 +108,7 @@ private static KeyStore initializeKeyStore(String keyStoreName, char[] keyStoreP try { in = FileUtil.open(SSLContextFactory.class, keyStoreName); if (in == null) { - log.warn(keyStoreName + ": keystore not found, using empty keystore"); + log.warn("{}: keystore not found, using empty keystore", keyStoreName); } keyStore.load(in, keyStorePassword); } finally { diff --git a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLSupport.java b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLSupport.java index d905eb7d8..c230930c4 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLSupport.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLSupport.java @@ -19,16 +19,15 @@ package quickfix.mina.ssl; -import java.io.IOException; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; - import quickfix.ConfigError; import quickfix.FieldConvertError; import quickfix.SessionID; import quickfix.SessionSettings; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import java.io.IOException; + public class SSLSupport { public static final String FILTER_NAME = "SslFilter"; public static final String SETTING_USE_SSL = "SocketUseSSL"; @@ -122,8 +121,7 @@ private static String getString(SessionSettings sessionSettings, SessionID sessi if (sessionSettings.isSetting(sessionID, key)) { try { propertyValue = sessionSettings.getString(sessionID, key); - } catch (ConfigError ignored) { - } catch (FieldConvertError ignored) { + } catch (ConfigError | FieldConvertError ignored) { } } return propertyValue; diff --git a/quickfixj-core/src/main/java/quickfix/mina/ssl/X509TrustManagerWrapper.java b/quickfixj-core/src/main/java/quickfix/mina/ssl/X509TrustManagerWrapper.java index 8c273a2ab..6a52c914b 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ssl/X509TrustManagerWrapper.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/X509TrustManagerWrapper.java @@ -48,7 +48,7 @@ public static TrustManager[] wrap(TrustManager[] trustManagers) { return wrappers; } - private X509TrustManager trustManager; + private final X509TrustManager trustManager; public X509TrustManagerWrapper(final X509TrustManager trustManager) { this.trustManager = trustManager; diff --git a/quickfixj-core/src/main/resources/config/sql/mysql/create_all.sql b/quickfixj-core/src/main/resources/config/sql/mysql/create_all.sql new file mode 100644 index 000000000..99bf6e3cf --- /dev/null +++ b/quickfixj-core/src/main/resources/config/sql/mysql/create_all.sql @@ -0,0 +1,84 @@ +DROP DATABASE IF EXISTS quickfix; +CREATE DATABASE quickfix; + + +USE quickfix; + +DROP TABLE IF EXISTS sessions; + +CREATE TABLE sessions ( +beginstring CHAR(8) NOT NULL, +sendercompid VARCHAR(64) NOT NULL, +sendersubid VARCHAR(64) NOT NULL, +senderlocid VARCHAR(64) NOT NULL, +targetcompid VARCHAR(64) NOT NULL, +targetsubid VARCHAR(64) NOT NULL, +targetlocid VARCHAR(64) NOT NULL, +session_qualifier VARCHAR(64) NOT NULL, +creation_time DATETIME NOT NULL, +incoming_seqnum INT NOT NULL, +outgoing_seqnum INT NOT NULL, +PRIMARY KEY (beginstring, sendercompid, sendersubid, senderlocid, + targetcompid, targetsubid, targetlocid, session_qualifier) +); + + +USE quickfix; + +DROP TABLE IF EXISTS messages; + +CREATE TABLE messages ( +beginstring CHAR(8) NOT NULL, +sendercompid VARCHAR(64) NOT NULL, +sendersubid VARCHAR(64) NOT NULL, +senderlocid VARCHAR(64) NOT NULL, +targetcompid VARCHAR(64) NOT NULL, +targetsubid VARCHAR(64) NOT NULL, +targetlocid VARCHAR(64) NOT NULL, +session_qualifier VARCHAR(64) NOT NULL, +msgseqnum INT NOT NULL, +message TEXT NOT NULL, +PRIMARY KEY (beginstring, sendercompid, sendersubid, senderlocid, + targetcompid, targetsubid, targetlocid, session_qualifier, + msgseqnum) +); + + +USE quickfix; + +DROP TABLE IF EXISTS messages_log; + +CREATE TABLE messages_log ( +id INT UNSIGNED NOT NULL AUTO_INCREMENT, +time DATETIME NOT NULL, +beginstring CHAR(8) NOT NULL, +sendercompid VARCHAR(64) NOT NULL, +sendersubid VARCHAR(64) NOT NULL, +senderlocid VARCHAR(64) NOT NULL, +targetcompid VARCHAR(64) NOT NULL, +targetsubid VARCHAR(64) NOT NULL, +targetlocid VARCHAR(64) NOT NULL, +session_qualifier VARCHAR(64) NOT NULL, +text TEXT NOT NULL, +PRIMARY KEY (id) +); + + +USE quickfix; + +DROP TABLE IF EXISTS event_log; + +CREATE TABLE event_log ( +id INT UNSIGNED NOT NULL AUTO_INCREMENT, +time DATETIME NOT NULL, +beginstring CHAR(8) NOT NULL, +sendercompid VARCHAR(64) NOT NULL, +sendersubid VARCHAR(64) NOT NULL, +senderlocid VARCHAR(64) NOT NULL, +targetcompid VARCHAR(64) NOT NULL, +targetsubid VARCHAR(64) NOT NULL, +targetlocid VARCHAR(64) NOT NULL, +session_qualifier VARCHAR(64), +text TEXT NOT NULL, +PRIMARY KEY (id) +); diff --git a/quickfixj-core/src/test/java/org/quickfixj/jmx/mbean/session/SessionAdminTest.java b/quickfixj-core/src/test/java/org/quickfixj/jmx/mbean/session/SessionAdminTest.java index 240cb5800..e5fcebc34 100644 --- a/quickfixj-core/src/test/java/org/quickfixj/jmx/mbean/session/SessionAdminTest.java +++ b/quickfixj-core/src/test/java/org/quickfixj/jmx/mbean/session/SessionAdminTest.java @@ -1,7 +1,11 @@ package org.quickfixj.jmx.mbean.session; import junit.framework.TestCase; -import quickfix.*; +import quickfix.Message; +import quickfix.Session; +import quickfix.SessionFactoryTestSupport; +import quickfix.SessionID; +import quickfix.SessionNotFound; import quickfix.field.NewSeqNo; import javax.management.ObjectName; @@ -16,15 +20,16 @@ public class SessionAdminTest extends TestCase { public void testResetSequence() throws Exception { - Session session = SessionFactoryTestSupport.createSession(); - MockSessionAdmin admin = new MockSessionAdmin(session, null, null); - admin.resetSequence(25); - assertEquals(1, admin.sentMessages.size()); - assertEquals(25, admin.sentMessages.get(0).getInt(NewSeqNo.FIELD)); + try (Session session = SessionFactoryTestSupport.createSession()) { + MockSessionAdmin admin = new MockSessionAdmin(session, null, null); + admin.resetSequence(25); + assertEquals(1, admin.sentMessages.size()); + assertEquals(25, admin.sentMessages.get(0).getInt(NewSeqNo.FIELD)); + } } private class MockSessionAdmin extends SessionAdmin { - final ArrayList sentMessages = new ArrayList(); + final ArrayList sentMessages = new ArrayList<>(); public MockSessionAdmin(Session session, ObjectName connectorName, ObjectName settingsName) { super(session, connectorName, settingsName); diff --git a/quickfixj-core/src/test/java/quickfix/AbstractMessageStoreTest.java b/quickfixj-core/src/test/java/quickfix/AbstractMessageStoreTest.java index be7e059ef..f4a54a73a 100644 --- a/quickfixj-core/src/test/java/quickfix/AbstractMessageStoreTest.java +++ b/quickfixj-core/src/test/java/quickfix/AbstractMessageStoreTest.java @@ -19,11 +19,11 @@ package quickfix; +import junit.framework.TestCase; + import java.io.IOException; import java.util.ArrayList; -import junit.framework.TestCase; - public abstract class AbstractMessageStoreTest extends TestCase { private SessionID sessionID; private MessageStore store; @@ -106,7 +106,7 @@ public void testMessageStorageMessages() throws Exception { store.refresh(); - final ArrayList messages = new ArrayList(); + final ArrayList messages = new ArrayList<>(); store.get(100, 115, messages); assertEquals("wrong # of messages", 2, messages.size()); assertEquals("wrong message", "\u00E4bcf\u00F6d\u00E7\u00E9", messages.get(0)); @@ -123,7 +123,7 @@ public void testMessageStorageOutOfSequence() throws Exception { store.refresh(); - final ArrayList messages = new ArrayList(); + final ArrayList messages = new ArrayList<>(); store.get(100, 115, messages); assertEquals("wrong # of messages", 2, messages.size()); assertEquals("wrong message", "message1", messages.get(0)); diff --git a/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java b/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java index 504860ca5..968d017da 100644 --- a/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java +++ b/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java @@ -19,20 +19,9 @@ package quickfix; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.io.ByteArrayInputStream; -import java.math.BigDecimal; -import java.net.URL; -import java.net.URLClassLoader; - import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; - import quickfix.field.Account; import quickfix.field.AvgPx; import quickfix.field.BodyLength; @@ -43,6 +32,7 @@ import quickfix.field.MsgSeqNum; import quickfix.field.MsgType; import quickfix.field.NoHops; +import quickfix.field.NoPartyIDs; import quickfix.field.NoRelatedSym; import quickfix.field.OrdType; import quickfix.field.OrderQty; @@ -51,6 +41,7 @@ import quickfix.field.SenderCompID; import quickfix.field.SenderSubID; import quickfix.field.SendingTime; +import quickfix.field.SessionRejectReason; import quickfix.field.Side; import quickfix.field.Symbol; import quickfix.field.TargetCompID; @@ -59,6 +50,18 @@ import quickfix.fix44.NewOrderSingle; import quickfix.test.util.ExpectedTestFailure; +import java.io.ByteArrayInputStream; +import java.math.BigDecimal; +import java.net.URL; +import java.net.URLClassLoader; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasProperty; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + public class DataDictionaryTest { @Rule @@ -225,6 +228,500 @@ public void testHeaderTrailerRequired() throws Exception { assertFalse("Unknown trailer field shows up as required", dd.isRequiredTrailerField(666)); } + @Test + public void testMessageWithNoChildren40() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields found: msgType=msg"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testMessageWithTextElement40() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields found: msgType=msg"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testMessagesWithNoChildren40() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No messages defined"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testMessagesWithTextElement40() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No messages defined"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testHeaderWithNoChildren40() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += ""; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields found: msgType=HEADER"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testHeaderWithTextElement40() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields found: msgType=HEADER"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testTrailerWithNoChildren40() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields found: msgType=TRAILER"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testTrailerWithTextElement40() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields found: msgType=TRAILER"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testFieldsWithNoChildren40() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields defined"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testFieldsWithTextElement40() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields defined"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testMessageWithNoChildren50() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields found: msgType=msg"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testMessageWithTextElement50() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields found: msgType=msg"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testMessagesWithNoChildren50() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No messages defined"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testMessagesWithTextElement50() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No messages defined"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testHeaderWithNoChildren50() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += ""; + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testHeaderWithTextElement50() 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())); + } + + @Test + public void testTrailerWithNoChildren50() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
    "; + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testTrailerWithTextElement50() 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())); + } + + @Test + public void testFieldsWithNoChildren50() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += ""; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields defined"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + + @Test + public void testFieldsWithTextElement50() throws Exception { + String data = ""; + data += ""; + data += "
    "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += ""; + + expectedException.expect(ConfigError.class); + expectedException.expectMessage("No fields defined"); + + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } + @Test public void testHeaderGroupField() throws Exception { DataDictionary dd = getDictionary(); @@ -476,6 +973,16 @@ public void testCopy() throws Exception { assertEquals(ddCopy.isCheckUnorderedGroupFields(),dataDictionary.isCheckUnorderedGroupFields()); assertEquals(ddCopy.isCheckUserDefinedFields(),dataDictionary.isCheckUserDefinedFields()); + 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()); } /** @@ -738,10 +1245,36 @@ private Message createQuoteRequest() { Message quoteRequest = new Message(); quoteRequest.getHeader().setString(MsgType.FIELD, MsgType.QUOTE_REQUEST); quoteRequest.setString(QuoteReqID.FIELD, "QR-12345"); - quoteRequest.addGroup(new Group(NoRelatedSym.FIELD, Symbol.FIELD)); + final Group noRelatedSymGroup = new Group(NoRelatedSym.FIELD, Symbol.FIELD); + noRelatedSymGroup.setString(Symbol.FIELD, "AAPL"); + quoteRequest.addGroup(noRelatedSymGroup); return quoteRequest; } + /** + * Dictionary "FIX44.xml":
    + *
    +     * message name=QuoteRequest msgtype=R msgcat=app
    +     *   group name=NoRelatedSym required=Y
    +     *     component name=Instrument required=Y
    +     *       field name=Symbol required=Y
    +     * 
    + * Field Symbol(55) is required, so validation must fail. + * @throws Exception + */ + @Test + public void testGroupWithReqdComponentWithReqdFieldValidation() throws Exception { + final Message quoteRequest = createQuoteRequest(); + quoteRequest.getGroup(1, NoRelatedSym.FIELD).removeField(Symbol.FIELD); + final DataDictionary dictionary = getDictionary(); + + expectedException.expect(FieldException.class); + expectedException.expect(hasProperty("sessionRejectReason", is(SessionRejectReason.REQUIRED_TAG_MISSING))); + expectedException.expect(hasProperty("field", is(Symbol.FIELD))); + + dictionary.validate(quoteRequest, true); + } + // // Group Validation Tests in RepeatingGroupTest // diff --git a/quickfixj-core/src/test/java/quickfix/DefaultMessageFactoryTest.java b/quickfixj-core/src/test/java/quickfix/DefaultMessageFactoryTest.java index 2af41cfe8..1fec31694 100644 --- a/quickfixj-core/src/test/java/quickfix/DefaultMessageFactoryTest.java +++ b/quickfixj-core/src/test/java/quickfix/DefaultMessageFactoryTest.java @@ -2,13 +2,13 @@ import static org.junit.Assert.*; import static quickfix.FixVersions.*; +import static quickfix.field.ApplVerID.*; import org.junit.Test; -import quickfix.field.LinesOfText; -import quickfix.field.MsgType; -import quickfix.field.NoLinesOfText; -import quickfix.field.NoMDEntries; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import quickfix.field.*; import quickfix.test.util.ExpectedTestFailure; /** @@ -17,8 +17,19 @@ * @author toli * @version $Id$ */ +@RunWith(Parameterized.class) public class DefaultMessageFactoryTest { - private final DefaultMessageFactory factory = new DefaultMessageFactory(); + private final DefaultMessageFactory factory; + private final Class fixtCreateExpectedClass; + + public DefaultMessageFactoryTest(String defaultApplVerID, Class fixtCreateExpectedClass) { + if (defaultApplVerID != null) { + this.factory = new DefaultMessageFactory(defaultApplVerID); + } else { + this.factory = new DefaultMessageFactory(); + } + this.fixtCreateExpectedClass = fixtCreateExpectedClass; + } @Test public void testMessageCreate() throws Exception { @@ -27,18 +38,28 @@ public void testMessageCreate() throws Exception { assertMessage(quickfix.fix42.Advertisement.class, MsgType.ADVERTISEMENT, factory.create(BEGINSTRING_FIX42, MsgType.ADVERTISEMENT)); assertMessage(quickfix.fix43.Advertisement.class, MsgType.ADVERTISEMENT, factory.create(BEGINSTRING_FIX43, MsgType.ADVERTISEMENT)); assertMessage(quickfix.fix44.Advertisement.class, MsgType.ADVERTISEMENT, factory.create(BEGINSTRING_FIX44, MsgType.ADVERTISEMENT)); - assertMessage(quickfix.fix50.Advertisement.class, MsgType.ADVERTISEMENT, factory.create(FIX50, MsgType.ADVERTISEMENT)); + assertMessage(quickfix.fix50.Advertisement.class, MsgType.ADVERTISEMENT, factory.create(FixVersions.FIX50, MsgType.ADVERTISEMENT)); + assertMessage(quickfix.fix50sp1.Advertisement.class, MsgType.ADVERTISEMENT, factory.create(FixVersions.FIX50SP1, MsgType.ADVERTISEMENT)); + assertMessage(quickfix.fix50sp2.Advertisement.class, MsgType.ADVERTISEMENT, factory.create(FixVersions.FIX50SP2, MsgType.ADVERTISEMENT)); assertMessage(quickfix.Message.class, MsgType.ADVERTISEMENT, factory.create("unknown", MsgType.ADVERTISEMENT)); } @Test public void testFixtCreate() throws Exception { assertMessage(quickfix.fixt11.Logon.class, MsgType.LOGON, factory.create(BEGINSTRING_FIXT11, MsgType.LOGON)); + assertMessage(fixtCreateExpectedClass, MsgType.EMAIL, factory.create(BEGINSTRING_FIXT11, MsgType.EMAIL)); + assertMessage(quickfix.fix40.Email.class, MsgType.EMAIL, factory.create(BEGINSTRING_FIXT11, new ApplVerID(FIX40), MsgType.EMAIL)); + assertMessage(quickfix.fix41.Email.class, MsgType.EMAIL, factory.create(BEGINSTRING_FIXT11, new ApplVerID(FIX41), MsgType.EMAIL)); + assertMessage(quickfix.fix42.Email.class, MsgType.EMAIL, factory.create(BEGINSTRING_FIXT11, new ApplVerID(FIX42), MsgType.EMAIL)); + assertMessage(quickfix.fix43.Email.class, MsgType.EMAIL, factory.create(BEGINSTRING_FIXT11, new ApplVerID(FIX43), MsgType.EMAIL)); + assertMessage(quickfix.fix44.Email.class, MsgType.EMAIL, factory.create(BEGINSTRING_FIXT11, new ApplVerID(FIX44), MsgType.EMAIL)); + assertMessage(quickfix.fix50.Email.class, MsgType.EMAIL, factory.create(BEGINSTRING_FIXT11, new ApplVerID(ApplVerID.FIX50), MsgType.EMAIL)); + assertMessage(quickfix.fix50sp1.Email.class, MsgType.EMAIL, factory.create(BEGINSTRING_FIXT11, new ApplVerID(ApplVerID.FIX50SP1), MsgType.EMAIL)); + assertMessage(quickfix.fix50sp2.Email.class, MsgType.EMAIL, factory.create(BEGINSTRING_FIXT11, new ApplVerID(ApplVerID.FIX50SP2), MsgType.EMAIL)); } @Test public void testGroupCreate() throws Exception { - new ExpectedTestFailure(IllegalArgumentException.class, "unknown") { protected void execute() throws Throwable { factory.create("unknown", MsgType.NEWS, LinesOfText.FIELD); @@ -50,7 +71,9 @@ protected void execute() throws Throwable { assertEquals(quickfix.fix42.News.LinesOfText.class, factory.create(BEGINSTRING_FIX42, MsgType.NEWS, LinesOfText.FIELD).getClass()); assertEquals(quickfix.fix43.News.LinesOfText.class, factory.create(BEGINSTRING_FIX43, MsgType.NEWS, LinesOfText.FIELD).getClass()); assertEquals(quickfix.fix44.News.LinesOfText.class, factory.create(BEGINSTRING_FIX44, MsgType.NEWS, LinesOfText.FIELD).getClass()); - assertEquals(quickfix.fix50.News.NoLinesOfText.class, factory.create(FIX50, MsgType.NEWS, NoLinesOfText.FIELD).getClass()); + assertEquals(quickfix.fix50.News.NoLinesOfText.class, factory.create(FixVersions.FIX50, MsgType.NEWS, NoLinesOfText.FIELD).getClass()); + assertEquals(quickfix.fix50sp1.News.NoLinesOfText.class, factory.create(FixVersions.FIX50SP1, MsgType.NEWS, NoLinesOfText.FIELD).getClass()); + assertEquals(quickfix.fix50sp2.News.NoLinesOfText.class, factory.create(FixVersions.FIX50SP2, MsgType.NEWS, NoLinesOfText.FIELD).getClass()); assertNull("if group can't be created return null", factory.create(BEGINSTRING_FIX40, MsgType.MARKET_DATA_SNAPSHOT_FULL_REFRESH, NoMDEntries.FIELD)); } @@ -59,4 +82,18 @@ private static void assertMessage(Class expectedMessageClass, String expected assertEquals(expectedMessageClass, message.getClass()); assertEquals(expectedMessageType, message.getHeader().getString(MsgType.FIELD)); } + + @Parameterized.Parameters(name = "defaultApplVerID = {0}") + public static Object[][] getParameters() { + return new Object[][] { + {ApplVerID.FIX40, quickfix.fix40.Email.class}, + {ApplVerID.FIX41, quickfix.fix41.Email.class}, + {ApplVerID.FIX42, quickfix.fix42.Email.class}, + {ApplVerID.FIX43, quickfix.fix43.Email.class}, + {ApplVerID.FIX44, quickfix.fix44.Email.class}, + {ApplVerID.FIX50, quickfix.fix50.Email.class}, + {ApplVerID.FIX50SP1, quickfix.fix50sp1.Email.class}, + {ApplVerID.FIX50SP2, quickfix.fix50sp2.Email.class} + }; + } } diff --git a/quickfixj-core/src/test/java/quickfix/DefaultSessionFactoryTest.java b/quickfixj-core/src/test/java/quickfix/DefaultSessionFactoryTest.java index e35337dbe..a991f559b 100644 --- a/quickfixj-core/src/test/java/quickfix/DefaultSessionFactoryTest.java +++ b/quickfixj-core/src/test/java/quickfix/DefaultSessionFactoryTest.java @@ -19,18 +19,20 @@ package quickfix; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - +import java.io.IOException; import org.junit.Before; import org.junit.Test; - import quickfix.field.ApplVerID; import quickfix.test.acceptance.ATApplication; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import org.junit.After; +import static org.junit.Assert.*; + public class DefaultSessionFactoryTest { private SessionID sessionID; @@ -42,7 +44,12 @@ public void setUp() throws Exception { sessionID = new SessionID(FixVersions.BEGINSTRING_FIX42, "SENDER", "TARGET"); setUpDefaultSettings(sessionID); factory = new DefaultSessionFactory(new ATApplication(), new MemoryStoreFactory(), - new ScreenLogFactory(true, true, true)); + new SLF4JLogFactory(new SessionSettings())); + } + + @After + public void tearDown() { + Session.unregisterSession(sessionID, true); } @Test @@ -55,7 +62,7 @@ public void testFixTMinimalSettings() throws Exception { sessionID = new SessionID(FixVersions.BEGINSTRING_FIXT11, "SENDER", "TARGET"); setUpDefaultSettings(sessionID); factory = new DefaultSessionFactory(new ATApplication(), new MemoryStoreFactory(), - new ScreenLogFactory(true, true, true)); + new SLF4JLogFactory(new SessionSettings())); Exception e = null; try { factory.create(sessionID, settings); @@ -86,29 +93,31 @@ public void testFixtDataDictionaryConfiguration() throws Exception { settings.setString(sessionID, Session.SETTING_APP_DATA_DICTIONARY, "FIX42.xml"); settings.setString(sessionID, Session.SETTING_APP_DATA_DICTIONARY + "." + FixVersions.BEGINSTRING_FIX40, "FIX40.xml"); - Session session = factory.create(sessionID, settings); - - DataDictionaryProvider provider = session.getDataDictionaryProvider(); - assertThat(provider.getSessionDataDictionary(sessionID.getBeginString()), - is(notNullValue())); + try (Session session = factory.create(sessionID, settings)) { - assertThat(provider.getApplicationDataDictionary(new ApplVerID(ApplVerID.FIX42)), - is(notNullValue())); - assertThat(provider.getApplicationDataDictionary(new ApplVerID(ApplVerID.FIX40)), - is(notNullValue())); + 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())); + } } @Test public void testPreFixtDataDictionaryConfiguration() throws Exception { settings.setBool(sessionID, Session.SETTING_USE_DATA_DICTIONARY, true); - Session session = factory.create(sessionID, settings); + 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())); + DataDictionaryProvider provider = session.getDataDictionaryProvider(); + assertThat(provider.getSessionDataDictionary(sessionID.getBeginString()), + is(notNullValue())); + assertThat(provider.getApplicationDataDictionary(new ApplVerID(ApplVerID.FIX42)), + is(notNullValue())); + } } @Test @@ -130,7 +139,7 @@ public void testUseDataDictionaryByDefault() throws Exception { createSessionAndAssertDictionaryNotFound(); } - private void createSessionAndAssertDictionaryNotFound() throws ConfigError { + private void createSessionAndAssertDictionaryNotFound() { try { factory.create(sessionID, settings); fail("no data dictionary exception"); @@ -181,13 +190,15 @@ public void testIncorrectTimeValues() throws Exception { @Test public void testTestRequestDelayMultiplier() throws Exception { settings.setString(sessionID, Session.SETTING_TEST_REQUEST_DELAY_MULTIPLIER, "0.37"); - Session session = factory.create(sessionID, settings); - assertEquals(0.37, session.getTestRequestDelayMultiplier(), 0); + try (Session session = factory.create(sessionID, settings)) { + assertEquals(0.37, session.getTestRequestDelayMultiplier(), 0); + } } private void createSessionAndAssertConfigError(String message, String pattern) { + Session session = null; try { - factory.create(sessionID, settings); + session = factory.create(sessionID, settings); fail(message); } catch (ConfigError e) { if (pattern != null) { @@ -196,6 +207,14 @@ private void createSessionAndAssertConfigError(String message, String pattern) { assertTrue("exception message not matched, expected: " + pattern + ", got: " + e.getMessage(), m.matches()); } + } finally { + if (session != null) { + try { + session.close(); + } catch (IOException ex) { + // ignore + } + } } } @@ -214,7 +233,25 @@ private void setUpDefaultSettings(SessionID sessionID) { @Test public void testReconnectIntervalInDefaultSession() throws Exception { settings.setString(sessionID, "ReconnectInterval", "2x5;3x15"); + Session session = factory.create(sessionID, settings); + session.close(); + } + + @Test + // QFJ-873 + public void testTimestampPrecision() throws Exception { + settings.setString(Session.SETTING_TIMESTAMP_PRECISION, "FOO"); + createSessionAndAssertConfigError("no exception", ".*No enum constant quickfix.UtcTimestampPrecision.FOO.*"); + settings.setString(Session.SETTING_TIMESTAMP_PRECISION, "SECONDS"); + factory.create(sessionID, settings); + settings.setString(Session.SETTING_TIMESTAMP_PRECISION, "MILLIS"); + factory.create(sessionID, settings); + settings.setString(Session.SETTING_TIMESTAMP_PRECISION, "NANOS"); + factory.create(sessionID, settings); + settings.setString(Session.SETTING_TIMESTAMP_PRECISION, "MICROS"); factory.create(sessionID, settings); + settings.setString(Session.SETTING_TIMESTAMP_PRECISION, "PICOS"); + createSessionAndAssertConfigError("no exception", ".*No enum constant quickfix.UtcTimestampPrecision.PICOS.*"); } } diff --git a/quickfixj-core/src/test/java/quickfix/ExceptionTest.java b/quickfixj-core/src/test/java/quickfix/ExceptionTest.java index 1f093e51a..7da22e93f 100644 --- a/quickfixj-core/src/test/java/quickfix/ExceptionTest.java +++ b/quickfixj-core/src/test/java/quickfix/ExceptionTest.java @@ -29,14 +29,13 @@ public void testDoNotSend() { public void testIncorrectDataFormat() { IncorrectDataFormat e = new IncorrectDataFormat(5, "test"); - assertEquals(5, e.field); - assertEquals("test", e.data); + assertEquals(5, e.getField()); + assertEquals("test", e.getData()); } public void testIncorrectTagValue() { new IncorrectTagValue(5); - IncorrectTagValue e = new IncorrectTagValue("test"); - e.field = 5; + IncorrectTagValue e = new IncorrectTagValue(5, "test"); } public void testRejectLogon() { diff --git a/quickfixj-core/src/test/java/quickfix/FieldConvertersTest.java b/quickfixj-core/src/test/java/quickfix/FieldConvertersTest.java index a245bace7..7facb53ff 100644 --- a/quickfixj-core/src/test/java/quickfix/FieldConvertersTest.java +++ b/quickfixj-core/src/test/java/quickfix/FieldConvertersTest.java @@ -19,6 +19,10 @@ package quickfix; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.ChronoField; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -211,6 +215,102 @@ public void testUtcTimeStampConversion() throws Exception { } catch (FieldConvertError e) { // expected } + + // just accept up to picoseconds but truncate after millis + date = UtcTimestampConverter.convert("20120922-12:34:56.123456"); + c.setTime(date); + assertEquals(12, c.get(Calendar.HOUR_OF_DAY)); + assertEquals(34, c.get(Calendar.MINUTE)); + assertEquals(56, c.get(Calendar.SECOND)); + assertEquals(2012, c.get(Calendar.YEAR)); + assertEquals(Calendar.SEPTEMBER, c.get(Calendar.MONTH)); + assertEquals(22, c.get(Calendar.DAY_OF_MONTH)); + assertEquals(123, c.get(Calendar.MILLISECOND)); + + date = UtcTimestampConverter.convert("20120922-12:34:56.123456789"); + c.setTime(date); + assertEquals(12, c.get(Calendar.HOUR_OF_DAY)); + assertEquals(34, c.get(Calendar.MINUTE)); + assertEquals(56, c.get(Calendar.SECOND)); + assertEquals(2012, c.get(Calendar.YEAR)); + assertEquals(Calendar.SEPTEMBER, c.get(Calendar.MONTH)); + assertEquals(22, c.get(Calendar.DAY_OF_MONTH)); + assertEquals(123, c.get(Calendar.MILLISECOND)); + + date = UtcTimestampConverter.convert("20120922-12:34:56.123456789123"); + c.setTime(date); + assertEquals(12, c.get(Calendar.HOUR_OF_DAY)); + assertEquals(34, c.get(Calendar.MINUTE)); + assertEquals(56, c.get(Calendar.SECOND)); + assertEquals(2012, c.get(Calendar.YEAR)); + assertEquals(Calendar.SEPTEMBER, c.get(Calendar.MONTH)); + assertEquals(22, c.get(Calendar.DAY_OF_MONTH)); + assertEquals(123, c.get(Calendar.MILLISECOND)); + + try { + UtcTimestampConverter.convert("20120922-12:34:56.12345"); + fail(); + } catch (FieldConvertError e) { + // expected + } + try { + UtcTimestampConverter.convert("20120922-12:34:56.1234567"); + fail(); + } catch (FieldConvertError e) { + // expected + } + + LocalDateTime dateTime = UtcTimestampConverter.convertToLocalDateTime("20120922-12:34:56"); + assertEquals(12, dateTime.getHour()); + assertEquals(34, dateTime.getMinute()); + assertEquals(56, dateTime.getSecond()); + assertEquals(2012, dateTime.getYear()); + assertEquals(9, dateTime.getMonthValue()); + assertEquals(22, dateTime.getDayOfMonth()); + assertEquals(0, dateTime.getLong(ChronoField.MILLI_OF_SECOND)); + assertEquals(0, dateTime.getLong(ChronoField.MICRO_OF_SECOND)); + assertEquals(0, dateTime.getNano()); + + dateTime = UtcTimestampConverter.convertToLocalDateTime("20120922-12:34:56.123456789111"); + assertEquals(12, dateTime.getHour()); + assertEquals(34, dateTime.getMinute()); + assertEquals(56, dateTime.getSecond()); + assertEquals(2012, dateTime.getYear()); + assertEquals(9, dateTime.getMonthValue()); + assertEquals(22, dateTime.getDayOfMonth()); + assertEquals(123, dateTime.getLong(ChronoField.MILLI_OF_SECOND)); + assertEquals(123456, dateTime.getLong(ChronoField.MICRO_OF_SECOND)); + assertEquals(123456789, dateTime.getNano()); + + dateTime = UtcTimestampConverter.convertToLocalDateTime("20120922-12:34:56.123456789"); + assertEquals(12, dateTime.getHour()); + assertEquals(34, dateTime.getMinute()); + assertEquals(56, dateTime.getSecond()); + assertEquals(2012, dateTime.getYear()); + assertEquals(9, dateTime.getMonthValue()); + assertEquals(22, dateTime.getDayOfMonth()); + assertEquals(123, dateTime.getLong(ChronoField.MILLI_OF_SECOND)); + assertEquals(123456, dateTime.getLong(ChronoField.MICRO_OF_SECOND)); + assertEquals(123456789, dateTime.getNano()); + + dateTime = UtcTimestampConverter.convertToLocalDateTime("20120922-12:34:56.123456"); + assertEquals(12, dateTime.getHour()); + assertEquals(34, dateTime.getMinute()); + assertEquals(56, dateTime.getSecond()); + assertEquals(2012, dateTime.getYear()); + assertEquals(9, dateTime.getMonthValue()); + assertEquals(22, dateTime.getDayOfMonth()); + assertEquals(123, dateTime.getLong(ChronoField.MILLI_OF_SECOND)); + assertEquals(123456, dateTime.getLong(ChronoField.MICRO_OF_SECOND)); + assertEquals(123456000, dateTime.getNano()); + + assertEquals("2012-09-22T12:34:56", UtcTimestampConverter.convertToLocalDateTime("20120922-12:34:56").toString()); + + assertEquals("20120922-12:34:56", UtcTimestampConverter.convert(dateTime, UtcTimestampPrecision.SECONDS)); + assertEquals("20120922-12:34:56.123", UtcTimestampConverter.convert(dateTime, UtcTimestampPrecision.MILLIS)); + assertEquals("20120922-12:34:56.123456", UtcTimestampConverter.convert(dateTime, UtcTimestampPrecision.MICROS)); + assertEquals("20120922-12:34:56.123456000", UtcTimestampConverter.convert(dateTime, UtcTimestampPrecision.NANOS)); + } public void testUtcTimeOnlyConversion() throws Exception { @@ -237,6 +337,65 @@ public void testUtcTimeOnlyConversion() throws Exception { } catch (FieldConvertError e) { // expected } + + date = UtcTimeOnlyConverter.convert("12:05:06.555444"); + c.setTime(date); + assertEquals(12, c.get(Calendar.HOUR_OF_DAY)); + assertEquals(5, c.get(Calendar.MINUTE)); + assertEquals(6, c.get(Calendar.SECOND)); + assertEquals(555, c.get(Calendar.MILLISECOND)); + assertEquals(1970, c.get(Calendar.YEAR)); + assertEquals(0, c.get(Calendar.MONTH)); + assertEquals(1, c.get(Calendar.DAY_OF_MONTH)); + + date = UtcTimeOnlyConverter.convert("12:05:06.555444333"); + c.setTime(date); + assertEquals(12, c.get(Calendar.HOUR_OF_DAY)); + assertEquals(5, c.get(Calendar.MINUTE)); + assertEquals(6, c.get(Calendar.SECOND)); + assertEquals(555, c.get(Calendar.MILLISECOND)); + assertEquals(1970, c.get(Calendar.YEAR)); + assertEquals(0, c.get(Calendar.MONTH)); + assertEquals(1, c.get(Calendar.DAY_OF_MONTH)); + + date = UtcTimeOnlyConverter.convert("12:05:06.555444333222"); + c.setTime(date); + assertEquals(12, c.get(Calendar.HOUR_OF_DAY)); + assertEquals(5, c.get(Calendar.MINUTE)); + assertEquals(6, c.get(Calendar.SECOND)); + assertEquals(555, c.get(Calendar.MILLISECOND)); + assertEquals(1970, c.get(Calendar.YEAR)); + assertEquals(0, c.get(Calendar.MONTH)); + assertEquals(1, c.get(Calendar.DAY_OF_MONTH)); + + UtcTimeOnlyConverter.convert("12:05:06"); + + try { + UtcTimeOnlyConverter.convert("12:05:06.55544"); + fail(); + } catch (FieldConvertError e) { + // expected + } + try { + UtcTimeOnlyConverter.convert("12:05:06.55544433"); + fail(); + } catch (FieldConvertError e) { + // expected + } + + LocalTime time = UtcTimeOnlyConverter.convertToLocalTime("12:05:06.555444333222"); + assertEquals(12, time.getHour()); + assertEquals(5, time.getMinute()); + assertEquals(6, time.getSecond()); + assertEquals(555, time.getLong(ChronoField.MILLI_OF_SECOND)); + assertEquals(555444, time.getLong(ChronoField.MICRO_OF_SECOND)); + assertEquals(555444333, time.getNano()); + + assertEquals("12:05:06", UtcTimeOnlyConverter.convert(time, UtcTimestampPrecision.SECONDS)); + assertEquals("12:05:06.555", UtcTimeOnlyConverter.convert(time, UtcTimestampPrecision.MILLIS)); + assertEquals("12:05:06.555444", UtcTimeOnlyConverter.convert(time, UtcTimestampPrecision.MICROS)); + assertEquals("12:05:06.555444333", UtcTimeOnlyConverter.convert(time, UtcTimestampPrecision.NANOS)); + } public void testUtcDateOnlyConversion() throws Exception { @@ -284,18 +443,15 @@ public void testUtcDateOnlyConversion() throws Exception { } catch (FieldConvertError e) { // expected } + + LocalDate localDate = UtcDateOnlyConverter.convertToLocalDate("20120922"); + assertEquals(2012, localDate.getYear()); + assertEquals(9, localDate.getMonthValue()); + assertEquals(22, localDate.getDayOfMonth()); + + LocalDate localDate2 = LocalDate.of(2012, 9, 20); + assertEquals("20120920", UtcDateOnlyConverter.convert(localDate2)); + } - // void FieldConvertorsTestCase::checkSumConvertTo::onRun( void*& ) - // { - // assert( CheckSumConvertor::convert( 0 ) == "000" ); - // assert( CheckSumConvertor::convert( 5 ) == "005" ); - // assert( CheckSumConvertor::convert( 12 ) == "012" ); - // assert( CheckSumConvertor::convert( 234 ) == "234" ); - // - // try{ CheckSumConvertor::convert( -1 ); assert( false ); } - // catch ( FieldConvertError& ) {} - // try{ CheckSumConvertor::convert( 256 ); assert( false ); } - // catch ( FieldConvertError& ) {}} - // } } diff --git a/quickfixj-core/src/test/java/quickfix/FieldMapTest.java b/quickfixj-core/src/test/java/quickfix/FieldMapTest.java index d92387ea2..c03f2cf7d 100644 --- a/quickfixj-core/src/test/java/quickfix/FieldMapTest.java +++ b/quickfixj-core/src/test/java/quickfix/FieldMapTest.java @@ -1,5 +1,8 @@ package quickfix; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneOffset; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; @@ -7,8 +10,8 @@ import quickfix.field.MDEntryTime; import quickfix.field.converter.UtcTimeOnlyConverter; -import java.util.Date; import java.util.Iterator; +import java.util.Optional; /** * Tests the {@link FieldMap} class. @@ -28,28 +31,28 @@ public static Test suite() { public void testSetUtcTimeStampField() throws Exception { FieldMap map = new Message(); - Date aDate = new Date(); + LocalDateTime aDate = LocalDateTime.now(); map.setField(new UtcTimeStampField(EffectiveTime.FIELD, aDate, false)); - assertEquals("milliseconds should not be preserved", aDate.getTime() - (aDate.getTime() % 1000), - map.getField(new EffectiveTime()).getValue().getTime()); + assertEquals("milliseconds should not be preserved", epochMilliOfLocalDate(aDate) - (epochMilliOfLocalDate(aDate) % 1000), + epochMilliOfLocalDate(map.getField(new EffectiveTime()).getValue())); // now set it with preserving millis map.setField(new UtcTimeStampField(EffectiveTime.FIELD, aDate, true)); - assertEquals("milliseconds should be preserved", aDate.getTime(), - map.getField(new EffectiveTime()).getValue().getTime()); + assertEquals("milliseconds should be preserved", epochMilliOfLocalDate(aDate), + epochMilliOfLocalDate(map.getField(new EffectiveTime()).getValue())); } public void testSetUtcTimeOnlyField() throws Exception { FieldMap map = new Message(); - Date aDate = new Date(); + LocalTime aDate = LocalTime.now(); map.setField(new UtcTimeOnlyField(MDEntryTime.FIELD, aDate, false)); - assertEquals("milliseconds should not be preserved", UtcTimeOnlyConverter.convert(aDate, false), - UtcTimeOnlyConverter.convert(map.getField(new MDEntryTime()).getValue(), false)); + assertEquals("milliseconds should not be preserved", UtcTimeOnlyConverter.convert(aDate, UtcTimestampPrecision.SECONDS), + UtcTimeOnlyConverter.convert(map.getField(new MDEntryTime()).getValue(), UtcTimestampPrecision.SECONDS)); // now set it with preserving millis map.setField(new UtcTimeOnlyField(MDEntryTime.FIELD, aDate, true)); - assertEquals("milliseconds should be preserved", UtcTimeOnlyConverter.convert(aDate, true), - UtcTimeOnlyConverter.convert(map.getField(new MDEntryTime()).getValue(), true)); + assertEquals("milliseconds should be preserved", UtcTimeOnlyConverter.convert(aDate, UtcTimestampPrecision.MILLIS), + UtcTimeOnlyConverter.convert(map.getField(new MDEntryTime()).getValue(), UtcTimestampPrecision.MILLIS)); } /** @@ -57,13 +60,14 @@ public void testSetUtcTimeOnlyField() throws Exception { */ public void testSpecificFields() throws Exception { FieldMap map = new Message(); - Date aDate = new Date(); + LocalDateTime aDate = LocalDateTime.now(); map.setField(new EffectiveTime(aDate)); - assertEquals("milliseconds should be preserved", aDate.getTime(), - map.getField(new EffectiveTime()).getValue().getTime()); - map.setField(new MDEntryTime(aDate)); - assertEquals("milliseconds should be preserved", UtcTimeOnlyConverter.convert(aDate, true), - UtcTimeOnlyConverter.convert(map.getField(new MDEntryTime()).getValue(), true)); + assertEquals("milliseconds should be preserved", epochMilliOfLocalDate(aDate), + epochMilliOfLocalDate(map.getField(new EffectiveTime()).getValue())); + LocalTime aTime = LocalTime.now(); + map.setField(new MDEntryTime(aTime)); + assertEquals("milliseconds should be preserved", UtcTimeOnlyConverter.convert(aTime, UtcTimestampPrecision.MILLIS), + UtcTimeOnlyConverter.convert(map.getField(new MDEntryTime()).getValue(), UtcTimestampPrecision.MILLIS)); } private void testOrdering(int[] vals, int[] order, int[] expected) { @@ -87,4 +91,18 @@ public void testOrdering() { 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 }); } + + public void testOptionalString() { + FieldMap map = new Message(); + map.setField(new StringField(128, "bigbank")); + Optional optValue = map.getOptionalString(128); + assertTrue(optValue.isPresent()); + assertEquals("bigbank", optValue.get()); + assertFalse(map.getOptionalString(129).isPresent()); + } + + + private long epochMilliOfLocalDate(LocalDateTime localDateTime) { + return localDateTime.toInstant(ZoneOffset.UTC).toEpochMilli(); + } } diff --git a/quickfixj-core/src/test/java/quickfix/FieldTest.java b/quickfixj-core/src/test/java/quickfix/FieldTest.java index 9eb1ef5d4..065ba419b 100644 --- a/quickfixj-core/src/test/java/quickfix/FieldTest.java +++ b/quickfixj-core/src/test/java/quickfix/FieldTest.java @@ -19,13 +19,6 @@ package quickfix; -import java.io.UnsupportedEncodingException; -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Date; - -import static junit.framework.Assert.assertTrue; -import static junit.framework.Assert.assertEquals; import org.junit.Test; import org.quickfixj.CharsetSupport; import quickfix.field.MDUpdateAction; @@ -34,6 +27,17 @@ import quickfix.field.TradeCondition; import quickfix.fix50.MarketDataIncrementalRefresh; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.Date; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; + public class FieldTest { @Test @@ -48,7 +52,7 @@ public void testMessageSetGetString() { } private void testFieldCalcuations(String value, int checksum, int length) { - Field field = new Field(12, value); + Field field = new Field<>(12, value); field.setObject(value); assertEquals("12=" + value, field.toString()); assertEquals(checksum, field.getChecksum()); @@ -98,7 +102,7 @@ public void testDateField() { @Test public void testUtcDateOnlyField() { UtcDateOnlyField field = new UtcDateOnlyField(11); - Date date = new Date(); + LocalDate date = LocalDate.now(); field.setValue(date); assertEquals(11, field.getTag()); assertEquals(date, field.getValue()); @@ -110,7 +114,7 @@ public void testUtcDateOnlyField() { @Test public void testUtcTimeOnlyField() { UtcTimeOnlyField field = new UtcTimeOnlyField(11); - Date date = new Date(); + LocalTime date = LocalTime.now(); field.setValue(date); assertEquals(11, field.getTag()); assertEquals(date, field.getValue()); @@ -122,7 +126,7 @@ public void testUtcTimeOnlyField() { @Test public void testUtcTimeStampField() { UtcTimeStampField field = new UtcTimeStampField(11); - Date date = new Date(); + LocalDateTime date = LocalDateTime.now(); field.setValue(date); assertEquals(11, field.getTag()); assertEquals(date, field.getValue()); @@ -247,9 +251,9 @@ public void testFieldhashCode() throws Exception { assertEqualsAndHash(new StringField(11, "foo"), new StringField(11, "foo")); assertEqualsAndHash(new BooleanField(11, true), new BooleanField(11, true)); assertEqualsAndHash(new CharField(11, 'x'), new CharField(11, 'x')); - Date date = new Date(); - assertEqualsAndHash(new UtcDateOnlyField(11, date), new UtcDateOnlyField(11, date)); - assertEqualsAndHash(new UtcTimeOnlyField(11, date), new UtcTimeOnlyField(11, date)); + LocalDateTime date = LocalDateTime.now(); + assertEqualsAndHash(new UtcDateOnlyField(11, date.toLocalDate()), new UtcDateOnlyField(11, date.toLocalDate())); + assertEqualsAndHash(new UtcTimeOnlyField(11, date.toLocalTime()), new UtcTimeOnlyField(11, date.toLocalTime())); assertEqualsAndHash(new UtcTimeStampField(11, date), new UtcTimeStampField(11, date)); } diff --git a/quickfixj-core/src/test/java/quickfix/FileLogTest.java b/quickfixj-core/src/test/java/quickfix/FileLogTest.java index 109964b64..cf524424c 100644 --- a/quickfixj-core/src/test/java/quickfix/FileLogTest.java +++ b/quickfixj-core/src/test/java/quickfix/FileLogTest.java @@ -202,14 +202,15 @@ public void testLogErrorWhenFilesystemRemoved() throws IOException { settings.setBool(sessionID, FileLogFactory.SETTING_INCLUDE_MILLIS_IN_TIMESTAMP, false); FileLogFactory factory = new FileLogFactory(settings); - Session session = new Session(new UnitTestApplication(), new MemoryStoreFactory(), + try (Session session = new Session(new UnitTestApplication(), new MemoryStoreFactory(), sessionID, new DefaultDataDictionaryProvider(), null, factory, - new DefaultMessageFactory(), 0); - Session.registerSession(session); - - FileLog log = (FileLog) session.getLog(); - log.close(); - log.logIncoming("test"); - // no stack overflow exception thrown + new DefaultMessageFactory(), 0)) { + Session.registerSession(session); + + FileLog log = (FileLog) session.getLog(); + log.close(); + log.logIncoming("test"); + // no stack overflow exception thrown + } } } diff --git a/quickfixj-core/src/test/java/quickfix/FileStoreTest.java b/quickfixj-core/src/test/java/quickfix/FileStoreTest.java index 661833d20..861236896 100644 --- a/quickfixj-core/src/test/java/quickfix/FileStoreTest.java +++ b/quickfixj-core/src/test/java/quickfix/FileStoreTest.java @@ -30,7 +30,7 @@ protected void tearDown() throws Exception { super.tearDown(); FileStore fileStore = (FileStore) getStore(); try { - fileStore.deleteFiles(); + fileStore.closeAndDeleteFiles(); } catch (IOException e) { System.err.println(e.getMessage()); } @@ -55,7 +55,7 @@ public void testMessageIndexReset() throws Exception { store.set(2, "MESSAGE"); - List messages = new ArrayList(); + List messages = new ArrayList<>(); store.get(1, 1, messages); assertEquals(0, messages.size()); @@ -85,4 +85,37 @@ public void testInitialSessionCreationTime() throws Exception { Date creationTime2 = store.getCreationTime(); assertEquals("wrong time diff", 0, Math.abs(creationTime1.getTime() - creationTime2.getTime())); } + + public void testResetShouldNeverFail() throws Exception { + final MockSystemTimeSource mockSystemTimeSource = new MockSystemTimeSource(System.currentTimeMillis()); + SystemTime.setTimeSource(mockSystemTimeSource); + final FileStore store = (FileStore) getStore(); + final Thread thread = new Thread(() -> { + while (true) { + try { + store.set(0, "SettingSomething"); + if (Thread.interrupted()) { + break; + } + } catch (IOException e) { + // it is ok for this to fail + } + } + }); + thread.setDaemon(true); + thread.start(); + + Date creationTime = store.getCreationTime(); + for (int i = 0; i < 20; i++) { + mockSystemTimeSource.increment(1); + store.reset(); + final Date newCreationTime = store.getCreationTime(); + assertTrue(newCreationTime.after(creationTime)); + creationTime = newCreationTime; + } + SystemTime.setTimeSource(null); + + thread.interrupt(); + thread.join(); + } } diff --git a/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java b/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java index ec50a5da1..9c1d5147b 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java @@ -26,14 +26,25 @@ import javax.sql.DataSource; -import junit.framework.TestCase; - -public class JdbcLogTest extends TestCase { +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertTrue; +import static junit.framework.TestCase.fail; +import org.junit.After; +import org.junit.Test; + +public class JdbcLogTest { private JdbcLog log; private JdbcLogFactory logFactory; private Connection connection; private SessionID sessionID; + @After + public void tearDown() { + Session.unregisterSession(sessionID, true); + } + + @Test public void testLog() throws Exception { doLogTest(null); } @@ -60,6 +71,7 @@ private void doLogTest(DataSource dataSource) throws ClassNotFoundException, SQL assertEquals(0, getRowCount(connection, "event_log")); } + @Test public void testLogWithHeartbeatFiltering() throws Exception { setUpJdbcLog(false, null); @@ -83,6 +95,7 @@ public void testLogWithHeartbeatFiltering() throws Exception { * (such as we can't connect ot the DB, or the tables are missing) and doesn't try * to print failing exceptions recursively until the stack overflows */ + @Test public void testHandlesRecursivelyFailingException() throws Exception { setUpJdbcLog(false, null); diff --git a/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java b/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java index ccbfb3e3d..647e59522 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java @@ -19,6 +19,17 @@ package quickfix; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + import static quickfix.JdbcSetting.SETTING_JDBC_DS_NAME; import static quickfix.JdbcSetting.SETTING_JDBC_STORE_MESSAGES_TABLE_NAME; import static quickfix.JdbcSetting.SETTING_JDBC_STORE_SESSIONS_TABLE_NAME; @@ -30,18 +41,6 @@ import static quickfix.JdbcTestSupport.loadSQL; import static quickfix.JdbcUtil.close; -import java.io.IOException; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import javax.naming.Context; -import javax.naming.InitialContext; -import javax.naming.NamingException; -import javax.sql.DataSource; - public class JdbcStoreTest extends AbstractMessageStoreTest { private String initialContextFactory; @@ -114,7 +113,7 @@ public void testMessageStorageMessagesWithCustomMessagesTableName() throws Excep assertTrue("set failed", store.set(113, "message1")); assertTrue("set failed", store.set(120, "message3")); - ArrayList messages = new ArrayList(); + ArrayList messages = new ArrayList<>(); store.get(100, 115, messages); assertEquals("wrong # of messages", 2, messages.size()); assertEquals("wrong message", "message2", messages.get(0)); @@ -166,7 +165,7 @@ public void testMessageUpdate() throws Exception { assertTrue(store.set(1, "MESSAGE1")); assertTrue(store.set(1, "MESSAGE2")); - List messages = new ArrayList(); + List messages = new ArrayList<>(); store.get(1, 1, messages); assertEquals("MESSAGE2", messages.get(0)); } diff --git a/quickfixj-core/src/test/java/quickfix/LogUtilTest.java b/quickfixj-core/src/test/java/quickfix/LogUtilTest.java index 892dbdaf0..46c4168d2 100644 --- a/quickfixj-core/src/test/java/quickfix/LogUtilTest.java +++ b/quickfixj-core/src/test/java/quickfix/LogUtilTest.java @@ -19,13 +19,13 @@ package quickfix; +import junit.framework.TestCase; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.util.Date; -import junit.framework.TestCase; - public class LogUtilTest extends TestCase { protected void setUp() throws Exception { @@ -48,19 +48,17 @@ private void createSessionAndGenerateException(LogFactory mockLogFactory) throws settings.setString(Session.SETTING_START_TIME, "16:00:00"); settings.setString(Session.SETTING_END_TIME, "13:00:00"); SessionID sessionID = new SessionID("FIX.4.2", "SENDER", "TARGET"); - SessionSchedule schedule = new SessionSchedule(settings, sessionID); - Session session = new Session(null, new MessageStoreFactory() { - public MessageStore create(SessionID sessionID) { - try { - return new MemoryStore() { - public Date getCreationTime() throws IOException { - throw new IOException("test"); - } - }; - } catch (IOException e) { - // ignore - return null; - } + SessionSchedule schedule = new DefaultSessionSchedule(settings, sessionID); + Session session = new Session(null, sessionID1 -> { + try { + return new MemoryStore() { + public Date getCreationTime() throws IOException { + throw new IOException("test"); + } + }; + } catch (IOException e) { + // ignore + return null; } }, sessionID, null, schedule, mockLogFactory, null, 0); try { @@ -80,10 +78,6 @@ public Log create(SessionID sessionID) { public Log create(SessionID sessionID, String callerFQCN) { return log; } - - public Log create() { - throw new UnsupportedOperationException(); - } }; } diff --git a/quickfixj-core/src/test/java/quickfix/LoginTestCase.java b/quickfixj-core/src/test/java/quickfix/LoginTestCase.java index 129b33245..03dad7926 100644 --- a/quickfixj-core/src/test/java/quickfix/LoginTestCase.java +++ b/quickfixj-core/src/test/java/quickfix/LoginTestCase.java @@ -19,13 +19,13 @@ package quickfix; -import static org.junit.Assert.*; -import static quickfix.FixVersions.*; - import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; +import static org.junit.Assert.assertEquals; +import static quickfix.FixVersions.BEGINSTRING_FIX44; + public class LoginTestCase { public static void main(String[] args) { @@ -35,23 +35,20 @@ public static void main(String[] args) { } private static void login(final String senderCompID) { - new Thread(new Runnable() { - - public void run() { - try { - SessionSettings settings = createSettings(senderCompID); - SessionID sessionID = settings.sectionIterator().next(); - SocketInitiator initiator = new SocketInitiator(new TestApplication(sessionID), - new FileStoreFactory(settings), settings, - new ScreenLogFactory(settings), new DefaultMessageFactory()); - - System.out.println(senderCompID + ": starting initiator"); - initiator.start(); - - new CountDownLatch(1).await(); - } catch (Exception e) { - e.printStackTrace(); - } + new Thread(() -> { + try { + SessionSettings settings = createSettings(senderCompID); + SessionID sessionID = settings.sectionIterator().next(); + SocketInitiator initiator = new SocketInitiator(new TestApplication(sessionID), + new FileStoreFactory(settings), settings, + new SLF4JLogFactory(settings), new DefaultMessageFactory()); + + System.out.println(senderCompID + ": starting initiator"); + initiator.start(); + + new CountDownLatch(1).await(); + } catch (Exception e) { + e.printStackTrace(); } }).start(); } @@ -59,7 +56,7 @@ public void run() { private static SessionSettings createSettings(String senderCompID) { SessionSettings settings = new SessionSettings(); - Map defaults = new HashMap(); + Map defaults = new HashMap<>(); defaults.put("FileStorePath", "target/data/banzai"); defaults.put("ConnectionType", "initiator"); defaults.put("TargetCompID", "EXEC"); diff --git a/quickfixj-core/src/test/java/quickfix/MessageCrackerTest.java b/quickfixj-core/src/test/java/quickfix/MessageCrackerTest.java index 169d9710b..2056517d7 100644 --- a/quickfixj-core/src/test/java/quickfix/MessageCrackerTest.java +++ b/quickfixj-core/src/test/java/quickfix/MessageCrackerTest.java @@ -91,7 +91,7 @@ public void testInvokerException3() throws Exception { MessageCracker cracker = new MessageCracker() { @Handler public void handle(quickfix.fixt11.Logon logon, SessionID sessionID) throws IncorrectTagValue { - throw new IncorrectTagValue("test"); + throw new IncorrectTagValue(0, "test"); } }; diff --git a/quickfixj-core/src/test/java/quickfix/MessageTest.java b/quickfixj-core/src/test/java/quickfix/MessageTest.java index 1562f0a35..2a6ea8150 100644 --- a/quickfixj-core/src/test/java/quickfix/MessageTest.java +++ b/quickfixj-core/src/test/java/quickfix/MessageTest.java @@ -19,24 +19,12 @@ package quickfix; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.math.BigDecimal; -import java.util.Calendar; -import java.util.Date; -import java.util.TimeZone; - import org.junit.Test; - import org.quickfixj.CharsetSupport; import quickfix.field.Account; import quickfix.field.AllocAccount; import quickfix.field.AllocShares; +import quickfix.field.ApplExtID; import quickfix.field.ApplVerID; import quickfix.field.AvgPx; import quickfix.field.BeginString; @@ -109,6 +97,38 @@ import quickfix.fix44.component.Parties; import quickfix.fix50.MarketDataSnapshotFullRefresh; +import java.math.BigDecimal; +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.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import quickfix.field.LastPx; +import quickfix.field.LastQty; +import quickfix.field.LegPrice; +import quickfix.field.LegQty; +import quickfix.field.LegRefID; +import quickfix.field.LegSymbol; +import quickfix.field.MaturityMonthYear; +import quickfix.field.PreviouslyReported; +import quickfix.field.PutOrCall; +import quickfix.field.QuoteAckStatus; +import quickfix.field.SecurityReqID; +import quickfix.field.SecurityRequestResult; +import quickfix.field.SecurityResponseID; +import quickfix.field.StrikePrice; +import quickfix.field.Text; +import quickfix.field.TradeDate; +import quickfix.field.TradeReportID; +import quickfix.fix44.TradeCaptureReport; + public class MessageTest { @Test @@ -133,7 +153,7 @@ public void testTrailerFieldOrdering() throws Exception { private NewOrderSingle createNewOrderSingle() { return new NewOrderSingle(new ClOrdID("CLIENT"), new HandlInst( HandlInst.AUTOMATED_EXECUTION_ORDER_PUBLIC), new Symbol("ORCL"), - new Side(Side.BUY), new TransactTime(new Date(0)), new OrdType(OrdType.LIMIT)); + new Side(Side.BUY), new TransactTime(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC)), new OrdType(OrdType.LIMIT)); } @Test @@ -220,14 +240,16 @@ public void testEmbeddedMessage() throws Exception { private void doTestMessageWithEncodedField(String charset, String text) throws Exception { CharsetSupport.setCharset(charset); + NewOrderSingle order = createNewOrderSingle(); + NewOrderSingle.IS_STRING_EQUIVALENT = CharsetSupport.isStringEquivalent(CharsetSupport.getCharsetInstance()); try { - 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()); assertEquals(charset + " encoded field", text, msg.getString(EncodedText.FIELD)); } finally { CharsetSupport.setCharset(CharsetSupport.getDefaultCharset()); + NewOrderSingle.IS_STRING_EQUIVALENT = CharsetSupport.isStringEquivalent(CharsetSupport.getCharsetInstance()); } } @@ -240,7 +262,7 @@ public void testMessageWithEncodedField() throws Exception { doTestMessageWithEncodedField("ISO-2022-JP", text); doTestMessageWithEncodedField("Shift_JIS", text); doTestMessageWithEncodedField("GBK", text); - //doTestMessageWithEncodedField("UTF-16", text); // double-byte charset not supported yet +// doTestMessageWithEncodedField("UTF-16", text); // double-byte charset not supported yet } @Test @@ -569,6 +591,11 @@ public void testFix5HeaderFields() { assertTrue(Message.isHeaderField(CstmApplVerID.FIELD)); } + @Test + public void testApplExtIDIsHeaderField() { + assertTrue(Message.isHeaderField(ApplExtID.FIELD)); + } + @Test public void testCalculateStringWithNestedGroups() throws Exception { final NewOrderCross noc = new NewOrderCross(); @@ -1022,11 +1049,11 @@ public void testMessageSetGetUtcTimeStamp() { calendar.set(2002, 8, 6, 12, 34, 56); calendar.set(Calendar.MILLISECOND, 0); - final Date time = calendar.getTime(); + final LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(calendar.getTimeInMillis()), ZoneOffset.UTC); message.setUtcTimeStamp(8, time); try { - assertEquals(message.getUtcTimeStamp(8).getTime(), time.getTime()); + assertEquals(message.getUtcTimeStamp(8), time); } catch (final FieldNotFound e) { assertTrue("exception thrown", false); } @@ -1353,6 +1380,399 @@ public void testInvalidHeaderFields() throws Exception { assertTrue(msg.isSetField(Account.FIELD)); } + @Test + // QFJ-791 + public void testRepeatingGroupCount() throws Exception { + /* + * Prepare a very simple TradeCaptureReport message template and two + * legs. + */ + 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))); + tcr.getHeader().setField(new SenderCompID("SENDER")); + tcr.getHeader().setField(new TargetCompID("TARGET")); + tcr.getHeader().setField(new MsgSeqNum(1)); + tcr.getHeader().setField(new SendingTime(LocalDateTime.now(ZoneOffset.UTC))); + TradeCaptureReport.NoLegs leg1 = new TradeCaptureReport.NoLegs(); + leg1.setField(new LegSymbol("L1-XYZ")); + leg1.setField(new LegRefID("ABC1234-L1")); + leg1.setField(new LegQty(333)); + leg1.setField(new LegPrice(1.2345)); + TradeCaptureReport.NoLegs leg2 = new TradeCaptureReport.NoLegs(); + leg2.setField(new LegSymbol("L2-XYZ")); + leg2.setField(new LegRefID("ABC1234-L2")); + leg2.setField(new LegQty(777)); + leg2.setField(new LegPrice(2.3456)); + + /* + * Create a message from the template and add two legs. Convert the + * message to string and parse it. The parsed message should contain two + * legs. + */ + { + Message m1 = new Message(); + m1.getHeader().setFields(tcr.getHeader()); + m1.setFields(tcr); + m1.addGroup(leg1); + m1.addGroup(leg2); + + String s1 = m1.toString(); + Message parsed1 = new Message(s1, DataDictionaryTest.getDictionary()); + + assertEquals(s1, parsed1.toString()); + assertEquals(2, parsed1.getGroupCount(555)); + } + + /* + * Create a message from the template and add two legs, but the first + * leg contains the additional tag 58 (Text). Convert the message to + * string and parse it. The parsed message should also contain two legs. + */ + { + Message m2 = new Message(); + m2.getHeader().setFields(tcr.getHeader()); + m2.setFields(tcr); + + leg1.setField(new Text("TXT1")); // add unexpected tag to leg1 + m2.addGroup(leg1); + m2.addGroup(leg2); + + 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); + + assertEquals(s2, parsed2.toString()); + assertEquals(2, parsed2.getGroupCount(555)); + + /* + * If the above test failed, it means that a simple addition of an + * unexpected tag made the parsing logic fail pretty badly, as the + * number of legs is not 2. + */ + } + } + + @Test + // QFJ-791 + public void testUnknownFieldsInRepeatingGroupsAndValidation() 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))); + tcr.getHeader().setField(new SenderCompID("SENDER")); + tcr.getHeader().setField(new TargetCompID("TARGET")); + tcr.getHeader().setField(new MsgSeqNum(1)); + tcr.getHeader().setField(new SendingTime(LocalDateTime.now(ZoneOffset.UTC))); + tcr.setField(new Symbol("ABC")); + TradeCaptureReport.NoLegs leg1 = new TradeCaptureReport.NoLegs(); + leg1.setField(new LegSymbol("L1-XYZ")); + leg1.setField(new LegRefID("ABC1234-L1")); + leg1.setField(new LegQty(333)); + leg1.setField(new LegPrice(1.2345)); + TradeCaptureReport.NoLegs leg2 = new TradeCaptureReport.NoLegs(); + leg2.setField(new LegSymbol("L2-XYZ")); + leg2.setField(new LegRefID("ABC1234-L2")); + leg2.setField(new LegQty(777)); + leg2.setField(new LegPrice(2.3456)); + TradeCaptureReport.NoSides sides = new TradeCaptureReport.NoSides(); + sides.setField(new Side(Side.BUY)); + sides.setField(new OrderID("ID")); + + { + // will add a user-defined tag (i.e. greater than 5000) that is not defined in that group + Message m1 = new Message(); + m1.getHeader().setFields(tcr.getHeader()); + m1.setFields(tcr); + + leg1.setField(new StringField(10000, "TXT1")); // add unexpected tag to leg1 + m1.addGroup(leg1); + m1.addGroup(leg2); + m1.addGroup(sides); + + String s1 = m1.toString(); + DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); + // parsing without validation should succeed + Message parsed1 = new Message(s1, dictionary, false); + + // validation should fail + int failingTag = 0; + try { + dictionary.validate(parsed1); + } catch (FieldException e) { + failingTag = e.getField(); + } + assertEquals(10000, failingTag); + + // but without checking user-defined fields, validation should succeed + dictionary.setCheckUserDefinedFields(false); + dictionary.validate(parsed1); + + assertEquals(s1, parsed1.toString()); + assertEquals(2, parsed1.getGroupCount(555)); + } + + { + // will add a normal tag that is not in the dictionary for that group + Message m2 = new Message(); + m2.getHeader().setFields(tcr.getHeader()); + m2.setFields(tcr); + + leg1.removeField(10000); // remove user-defined tag from before + leg1.setField(new Text("TXT1")); // add unexpected tag to leg1 + + m2.addGroup(leg1); + m2.addGroup(leg2); + m2.addGroup(sides); + + String s2 = m2.toString(); + DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); + // parsing without validation should succeed + Message parsed2 = new Message(s2, dictionary, false); + + // validation should fail + int failingTag = 0; + try { + dictionary.validate(parsed2); + } 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); + + assertEquals(s2, parsed2.toString()); + assertEquals(2, parsed2.getGroupCount(555)); + } + } + + @Test + // QFJ-169 + public void testInvalidFieldInGroup() throws Exception { + SecurityRequestResult resultCode = new SecurityRequestResult( + SecurityRequestResult.NO_INSTRUMENTS_FOUND_THAT_MATCH_SELECTION_CRITERIA); + + UnderlyingSymbol underlyingSymbolField = new UnderlyingSymbol("UND"); + SecurityReqID id = new SecurityReqID("1234"); + + quickfix.fix44.DerivativeSecurityList responseMessage = new quickfix.fix44.DerivativeSecurityList(); + responseMessage.setField(id); + responseMessage.setField(underlyingSymbolField); + responseMessage.setField(new SecurityResponseID("2345")); + Group optionGroup = new quickfix.fix44.DerivativeSecurityList.NoRelatedSym(); + optionGroup.setField(new Symbol("OPT+RQ")); + optionGroup.setField(new StringField(StrikePrice.FIELD, "10")); + // add invalid field for this FIX version + optionGroup.setField(new QuoteAckStatus(0)); + optionGroup.setField(new PutOrCall(PutOrCall.CALL)); + optionGroup.setField(new MaturityMonthYear("200802")); + responseMessage.addGroup(optionGroup); + + Group group2 = new quickfix.fix44.DerivativeSecurityList.NoRelatedSym(); + group2.setField(new Symbol("OPT+RB")); + group2.setField(new StringField(StrikePrice.FIELD, "10")); + group2.setField(new MaturityMonthYear("200802")); + responseMessage.addGroup(group2); + resultCode.setValue(SecurityRequestResult.VALID_REQUEST); + responseMessage.setField(resultCode); + + DataDictionary dd = new DataDictionary(DataDictionaryTest.getDictionary()); + + int tagNo = 0; + try { + dd.validate(responseMessage, true); + } catch (FieldException e) { + tagNo = e.getField(); + } + // make sure that tag 297 is reported as invalid, NOT tag 55 + // (which is the first field after the invalid 297 field) + assertEquals(QuoteAckStatus.FIELD, tagNo); + + Message msg2 = new Message(responseMessage.toString(), dd); + try { + dd.validate(msg2, true); + } catch (FieldException e) { + tagNo = e.getField(); + } + // make sure that tag 297 is reported as invalid, NOT tag 55 + // (which is the first field after the invalid 297 field) + assertEquals(QuoteAckStatus.FIELD, tagNo); + + // parse message again without validation + msg2 = new Message(responseMessage.toString(), dd, false); + assertEquals(responseMessage.toString(), msg2.toString()); + Group noRelatedSymGroup = new quickfix.fix44.DerivativeSecurityList.NoRelatedSym(); + Group group = responseMessage.getGroup(1, noRelatedSymGroup); + assertTrue(group.isSetField(QuoteAckStatus.FIELD)); + + group = responseMessage.getGroup(2, noRelatedSymGroup); + assertFalse(group.isSetField(QuoteAckStatus.FIELD)); + } + + @Test + // QFJ-169/QFJ-791 + public void testNestedRepeatingGroup() + throws Exception { + + String newOrdersSingleString = "8=FIX.4.4|9=265|35=D|34=62|49=sender|52=20160803-12:55:42.094|" + + "56=target|11=16H03A0000021|15=CHF|22=4|38=13|40=2|44=132|48=CH000000000|54=1|55=[N/A]|59=0|" + + "60=20160803-12:55:41.866|207=XXXX|423=2|526=foo|528=P|" + // tag 20000 is not defined, tag 22000 is defined for NewOrderSingle in FIX44_Custom_Test.xml + + "453=1|448=test|447=D|452=7|20000=0|802=1|523=test|803=25|22000=foobar|10=244|"; + + 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); + assertNull(nos.getException()); + dataDictionary.validate(nos); + + // defined tag should be set on the message + assertTrue(nos.isSetField(22000)); + // undefined tag should not be set on the message + assertFalse(nos.isSetField(20000)); + Group partyGroup = nos.getGroup(1, quickfix.field.NoPartyIDs.FIELD); + // undefined tag should be set on the group instead + assertTrue(partyGroup.isSetField(20000)); + assertFalse(partyGroup.getGroup(1, quickfix.field.NoPartySubIDs.FIELD).isSetField(20000)); + } + + @Test + // QFJ-169/QFJ-791 + public void testNestedRepeatingSubGroup() + throws Exception { + + String newOrdersSingleString = "8=FIX.4.4|9=265|35=D|34=62|49=sender|52=20160803-12:55:42.094|" + + "56=target|11=16H03A0000021|15=CHF|22=4|38=13|40=2|44=132|48=CH000000000|54=1|55=[N/A]|59=0|" + + "60=20160803-12:55:41.866|207=XXXX|423=2|526=foo|528=P|" + // tag 20000 is not defined, tag 22000 is defined for NewOrderSingle in FIX44_Custom_Test.xml + + "453=1|448=test|447=D|452=7|802=1|523=test|803=25|20000=0|22000=foobar|10=244|"; + + 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); + assertNull(nos.getException()); + dataDictionary.validate(nos); + + // defined tag should be set on the message + assertTrue(nos.isSetField(22000)); + // undefined tag should not be set on the message + assertFalse(nos.isSetField(20000)); + Group partyGroup = nos.getGroup(1, quickfix.field.NoPartyIDs.FIELD); + // undefined tag should be set on the subgroup instead + assertFalse(partyGroup.isSetField(20000)); + assertTrue(partyGroup.getGroup(1, quickfix.field.NoPartySubIDs.FIELD).isSetField(20000)); + } + + @Test + // QFJ-792 + public void testRepeatingGroupCountForIncorrectFieldOrder() throws Exception { + // correct order would be 600, 687, 654, 566 + testRepeatingGroupCountForFieldOrder(new int[]{600, 687, 566, 654}); + } + + private void testRepeatingGroupCountForFieldOrder(int fieldOrder[]) throws Exception { + /* + * Prepare a very simple TradeCaptureReport message template with 1 + * repeating group. + */ + Message tcr = new TradeCaptureReport(); + tcr.getHeader().setField(new MsgSeqNum(1)); + tcr.getHeader().setField(new SendingTime(LocalDateTime.now(ZoneOffset.UTC))); + tcr.getHeader().setField(new SenderCompID("SENDER")); + tcr.getHeader().setField(new TargetCompID("TARGET")); + tcr.setField(new TradeReportID("ABC1234")); + tcr.setField(new PreviouslyReported(false)); + tcr.setField(new LastQty(1000)); + tcr.setField(new LastPx(5.6789)); + tcr.setField(new TradeDate("20140101")); + tcr.setField(new TransactTime(LocalDateTime.now(ZoneOffset.UTC))); + Group leg1 = new Group(555, 600, fieldOrder); + leg1.setField(new LegSymbol("L1-XYZ")); + leg1.setField(new LegRefID("ABC1234-L1")); + leg1.setField(new LegQty(333)); + leg1.setField(new LegPrice(1.2345)); + tcr.addGroup(leg1); + /* + * Convert the message to string and parse it. The parsed message should + * contain 1 repeating group. + */ + String s = tcr.toString(); + DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); + dictionary.setCheckUnorderedGroupFields(false); + // without checking order of repeating group it should work + Message parsed = new Message(s, dictionary); + FieldException exception = parsed.getException(); + assertNull(exception); + + assertEquals(1, parsed.getGroupCount(555)); + + dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); + // when checking order of repeating group, an error should be reported + parsed = new Message(s, dictionary); + exception = parsed.getException(); + assertEquals(654, exception.getField()); + // but we still should have the repeating group set and not ignore it + assertEquals(1, parsed.getGroupCount(555)); + } + + // QFJ-533 + @Test + public void testRepeatingGroupCountWithNonIntegerValues() throws Exception { + DataDictionary dictionary = new DataDictionary(DataDictionaryTest.getDictionary()); + 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); + } catch (final InvalidMessage im) { + assertNotNull("InvalidMessage correctly thrown", im); + } catch (final Throwable e) { + e.printStackTrace(); + fail("InvalidMessage expected, got " + e.getClass().getName()); + } + } + + + // QFJ-770/QFJ-792 + @Test + public void testRepeatingGroupCountWithUnknownFields() throws Exception { + String test = "8=FIX.4.4|9=431|35=d|49=1|34=2|52=20140117-18:20:26.629|56=3|57=21|322=388721|" + + "323=4|320=1|393=42|82=1|67=1|711=1|311=780508|309=text|305=8|463=FXXXXX|307=text|542=20140716|" + + "436=10.0|9013=1.0|9014=1.0|9017=10|9022=1|9024=1.0|9025=Y|916=20140701|917=20150731|9201=23974|" + + "9200=17|9202=text|9300=727|9301=text|9302=text|9303=text|998=text|9100=text|9101=text|9085=text|" + + "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()); + Message message = new Message(); + message.fromString(test.replaceAll("\\|", "\001"), dictionary, true); + Group group = message.getGroup(1, 711); + String underlyingSymbol = group.getString(311); + assertEquals("780508", underlyingSymbol); + } + + @Test + // QFJ-940 + public void testRawString() throws Exception { + + String test = "8=FIX.4.4|9=431|35=d|49=1|34=2|52=20140117-18:20:26.629|56=3|57=21|322=388721|" + + "323=4|320=1|393=42|82=1|67=1|711=1|311=780508|309=text|305=8|463=FXXXXX|307=text|542=20140716|" + + "436=10.0|9013=1.0|9014=1.0|9017=10|9022=1|9024=1.0|9025=Y|916=20140701|917=20150731|9201=23974|" + + "9200=17|9202=text|9300=727|9301=text|9302=text|9303=text|998=text|9100=text|9101=text|9085=text|" + + "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()); + Message message = new Message(); + message.fromString(test.replaceAll("\\|", "\001"), dictionary, true); + assertEquals(test, message.toRawString().replaceAll("\001", "\\|")); + } + private void assertHeaderField(Message message, String expectedValue, int field) throws FieldNotFound { assertEquals(expectedValue, message.getHeader().getString(field)); @@ -1414,14 +1834,18 @@ private void assertGroupContent(Message message, NewOrderSingle.NoAllocs numAllo } private void assertAllocation(String accountId, Object shares) { - if (accountId.equals("AllocACC1")) { - assertEquals("got shares: " + shares, 0, - new BigDecimal("1010.10").compareTo(new BigDecimal(shares.toString()))); - } else if (accountId.equals("AllocACC2")) { - assertEquals("got shares: " + shares, 0, - new BigDecimal("2020.20").compareTo(new BigDecimal(shares.toString()))); - } else { - fail("Unknown account"); + switch (accountId) { + case "AllocACC1": + assertEquals("got shares: " + shares, 0, + new BigDecimal("1010.10").compareTo(new BigDecimal(shares.toString()))); + break; + case "AllocACC2": + assertEquals("got shares: " + shares, 0, + new BigDecimal("2020.20").compareTo(new BigDecimal(shares.toString()))); + break; + default: + fail("Unknown account"); + break; } } diff --git a/quickfixj-core/src/test/java/quickfix/MockSystemTimeSource.java b/quickfixj-core/src/test/java/quickfix/MockSystemTimeSource.java index 562e6ae92..b092e9136 100644 --- a/quickfixj-core/src/test/java/quickfix/MockSystemTimeSource.java +++ b/quickfixj-core/src/test/java/quickfix/MockSystemTimeSource.java @@ -19,6 +19,9 @@ package quickfix; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Calendar; public class MockSystemTimeSource implements SystemTimeSource { @@ -45,6 +48,7 @@ public void setTime(Calendar c) { setSystemTimes(c.getTimeInMillis()); } + @Override public long getTime() { if (systemTimes.length - offset > 1) { offset++; @@ -57,4 +61,11 @@ public void increment(long delta) { systemTimes[offset] += delta; } } + + @Override + public LocalDateTime getNow() { + // TODO maybe we need nano-precision later on + return LocalDateTime.ofInstant(Instant.ofEpochMilli(getTime()), ZoneOffset.UTC); + } + } diff --git a/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java b/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java index 5d78a109e..8b7edfd41 100644 --- a/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java +++ b/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java @@ -19,19 +19,17 @@ package quickfix; -import java.util.HashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - import junit.framework.TestCase; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import quickfix.field.TestReqID; import quickfix.fix42.TestRequest; import quickfix.mina.ProtocolFactory; +import java.util.HashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + public class MultiAcceptorTest extends TestCase { private final Logger log = LoggerFactory.getLogger(getClass()); private TestAcceptorApplication testAcceptorApplication; @@ -131,7 +129,7 @@ private SessionID getSessionIDForClient(int i) { } private static class TestAcceptorApplication extends ApplicationAdapter { - private final HashMap sessionMessages = new HashMap(); + private final HashMap sessionMessages = new HashMap<>(); private final CountDownLatch logonLatch; private CountDownLatch messageLatch; @@ -192,7 +190,7 @@ public void tearDown() { private Initiator createInitiator(boolean wrongPort) throws ConfigError { SessionSettings settings = new SessionSettings(); - HashMap defaults = new HashMap(); + HashMap defaults = new HashMap<>(); defaults.put("ConnectionType", "initiator"); defaults.put("StartTime", "00:00:00"); defaults.put("EndTime", "00:00:00"); @@ -208,7 +206,7 @@ private Initiator createInitiator(boolean wrongPort) throws ConfigError { configureInitiatorForSession(settings, 3, wrongPort ? 1000 : 10003); MessageStoreFactory factory = new MemoryStoreFactory(); - quickfix.LogFactory logFactory = new ScreenLogFactory(true, true, true); + quickfix.LogFactory logFactory = new SLF4JLogFactory(new SessionSettings()); return new SocketInitiator(new ApplicationAdapter() { }, factory, settings, logFactory, new DefaultMessageFactory()); } @@ -222,7 +220,7 @@ private void configureInitiatorForSession(SessionSettings settings, int i, int p private Acceptor createAcceptor() throws ConfigError { SessionSettings settings = new SessionSettings(); - HashMap defaults = new HashMap(); + HashMap defaults = new HashMap<>(); defaults.put("ConnectionType", "acceptor"); defaults.put("StartTime", "00:00:00"); defaults.put("EndTime", "00:00:00"); @@ -237,7 +235,7 @@ private Acceptor createAcceptor() throws ConfigError { configureAcceptorForSession(settings, 3, 10003); MessageStoreFactory factory = new MemoryStoreFactory(); - quickfix.LogFactory logFactory = new ScreenLogFactory(true, true, true); + quickfix.LogFactory logFactory = new SLF4JLogFactory(new SessionSettings()); return new SocketAcceptor(testAcceptorApplication, factory, settings, logFactory, new DefaultMessageFactory()); } diff --git a/quickfixj-core/src/test/java/quickfix/PausableThreadPoolExecutor.java b/quickfixj-core/src/test/java/quickfix/PausableThreadPoolExecutor.java new file mode 100644 index 000000000..4af2355a0 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/PausableThreadPoolExecutor.java @@ -0,0 +1,49 @@ +package quickfix; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +class PausableThreadPoolExecutor extends ThreadPoolExecutor { + private boolean isPaused; + private final ReentrantLock pauseLock = new ReentrantLock(); + private final Condition unpaused = pauseLock.newCondition(); + + public PausableThreadPoolExecutor() { + super(2, 2, 20, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10000)); + } + + protected void beforeExecute(Thread t, Runnable r) { + super.beforeExecute(t, r); + pauseLock.lock(); + try { + while (isPaused) + unpaused.await(); + } catch (InterruptedException ie) { + t.interrupt(); + } finally { + pauseLock.unlock(); + } + } + + public void pause() { + pauseLock.lock(); + try { + isPaused = true; + } finally { + pauseLock.unlock(); + } + } + + public void resume() { + pauseLock.lock(); + try { + isPaused = false; + unpaused.signalAll(); + } finally { + pauseLock.unlock(); + } + } +} diff --git a/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java b/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java index 1fd26df10..1132a307f 100644 --- a/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java +++ b/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java @@ -19,8 +19,10 @@ package quickfix; -import junit.framework.TestCase; import org.junit.Assert; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import org.junit.Test; import quickfix.field.BeginString; import quickfix.field.LegSymbol; import quickfix.field.OrderID; @@ -28,8 +30,9 @@ import quickfix.field.SettlDate2; import quickfix.field.Symbol; import quickfix.fix44.Quote; +import quickfix.fix50sp2.QuoteRequest; -public class RepeatingGroupTest extends TestCase { +public class RepeatingGroupTest { // In this testcase we use only FIX4.4 message, but we could use the others // FIX version. Indeed the group @@ -42,6 +45,7 @@ private Quote.NoLegs buildGroupWithStandardFields(String settingValue) { return grp; } + @Test public void testSettingGettingGroupWithStandardFields() throws FieldNotFound { final String settingValue = "SETTING_VALUE"; @@ -62,6 +66,7 @@ private Quote.NoLegs buildGroupWithCustomFields(String settingValue) { return grp; } + @Test public void testSettingGettingGroupWithCustomFields() throws FieldNotFound { final String settingValue = "SETTING_VALUE"; @@ -84,6 +89,7 @@ private Quote.NoLegs buildGroupWithCustomAndStandardFields(String settingValue) return grp; } + @Test public void testSettingGettingGroupWithCustomAndStandardFields() throws FieldNotFound { final String settingValue = "SETTING_VALUE"; @@ -123,25 +129,26 @@ private quickfix.fix44.QuoteRequest.NoRelatedSym buildNestedGroupWithStandardFie return gNoRelatedSym; } - private quickfix.fix50.QuoteRequest.NoRelatedSym buildNestedGroupWithStandardFieldsFIX50( + private quickfix.fix50sp2.QuoteRequest.NoRelatedSym buildNestedGroupWithStandardFieldsFIX50SP2( String settingValue) { // The root group - final quickfix.fix50.QuoteRequest.NoRelatedSym gNoRelatedSym = new quickfix.fix50.QuoteRequest.NoRelatedSym(); + final quickfix.fix50sp2.QuoteRequest.NoRelatedSym gNoRelatedSym = new quickfix.fix50sp2.QuoteRequest.NoRelatedSym(); // The nested group - final quickfix.fix50.QuoteRequest.NoRelatedSym.NoLegs nestedgroup = new quickfix.fix50.QuoteRequest.NoRelatedSym.NoLegs(); + final quickfix.fix50sp2.QuoteRequest.NoRelatedSym.NoLegs nestedgroup = new quickfix.fix50sp2.QuoteRequest.NoRelatedSym.NoLegs(); nestedgroup.setField(new LegSymbol(settingValue)); gNoRelatedSym.addGroup(nestedgroup); // Adding a second fake nested group to avoid being the case of having // one element which is not relevant :-) - final quickfix.fix50.QuoteRequest.NoRelatedSym.NoLegs oneMoreNestedgroup = new quickfix.fix50.QuoteRequest.NoRelatedSym.NoLegs(); + final quickfix.fix50sp2.QuoteRequest.NoRelatedSym.NoLegs oneMoreNestedgroup = new quickfix.fix50sp2.QuoteRequest.NoRelatedSym.NoLegs(); oneMoreNestedgroup.setField(new LegSymbol("Donald")); gNoRelatedSym.addGroup(oneMoreNestedgroup); return gNoRelatedSym; } + @Test public void testSettingGettingNestedGroupWithStandardFields() throws FieldNotFound { final String settingValue = "SETTING_VALUE"; @@ -176,6 +183,7 @@ private quickfix.fix44.QuoteRequest.NoRelatedSym buildNestedGroupWithCustomField return gNoRelatedSym; } + @Test public void testSettingGettingNestedGroupWithCustomFields() throws FieldNotFound { final String settingValue = "SETTING_VALUE"; @@ -213,6 +221,7 @@ private quickfix.fix44.QuoteRequest.NoRelatedSym buildNestedGroupWithCustomAndSt return gNoRelatedSym; } + @Test public void testSettingGettingNestedGroupWithCustomAndStandardFields() throws FieldNotFound { final String settingValue = "SETTING_VALUE"; @@ -234,6 +243,7 @@ public void testSettingGettingNestedGroupWithCustomAndStandardFields() throws Fi .getValue()); } + @Test // Testing group re-usability when setting values public void testSettingGettingGroupByReusingGroup() throws FieldNotFound { // The root group @@ -294,6 +304,7 @@ private Message buildValidatedMessage(String sourceFIXString, DataDictionary dd) return message; } + @Test public void testValidationWithNestedGroupAndStandardFields() throws InvalidMessage { final quickfix.fix44.QuoteRequest quoteRequest = new quickfix.fix44.QuoteRequest(); @@ -305,7 +316,6 @@ public void testValidationWithNestedGroupAndStandardFields() throws InvalidMessa gNoRelatedSym.setField(new Symbol("SYM00")); quoteRequest.addGroup(gNoRelatedSym); - quoteRequest.addGroup(gNoRelatedSym); final String sourceFIXString = quoteRequest.toString(); @@ -320,35 +330,34 @@ public void testValidationWithNestedGroupAndStandardFields() throws InvalidMessa assertEquals("Message validation failed", sourceFIXString, validateFIXString); } + @Test public void testValidationWithNestedGroupAndStandardFieldsFIX50SP2() throws InvalidMessage, ConfigError { - final quickfix.fix50.QuoteRequest quoteRequest = new quickfix.fix50.QuoteRequest(); + final quickfix.fix50sp2.QuoteRequest quoteRequest = new quickfix.fix50sp2.QuoteRequest(); final quickfix.field.QuoteReqID gQuoteReqID = new quickfix.field.QuoteReqID(); gQuoteReqID.setValue("12342"); quoteRequest.setField(gQuoteReqID); - final quickfix.fix50.QuoteRequest.NoRelatedSym gNoRelatedSym = buildNestedGroupWithStandardFieldsFIX50("DEFAULT_VALUE"); + final quickfix.fix50sp2.QuoteRequest.NoRelatedSym gNoRelatedSym = buildNestedGroupWithStandardFieldsFIX50SP2("DEFAULT_VALUE"); gNoRelatedSym.setField(new Symbol("SYM00")); gNoRelatedSym.setField(new SettlDate2("20120801")); quoteRequest.addGroup(gNoRelatedSym); - quoteRequest.addGroup(gNoRelatedSym); final String sourceFIXString = quoteRequest.toString(); - final DataDictionary fix50DataDictionary = new DataDictionary("FIX50SP2.xml"); - final quickfix.fix50.QuoteRequest validatedMessage = (quickfix.fix50.QuoteRequest) buildValidatedMessage( - sourceFIXString, fix50DataDictionary); - String validateFIXString = null; - if (validatedMessage != null) { - validateFIXString = validatedMessage.toString(); - } + final DataDictionary fix50sp2DataDictionary = new DataDictionary("FIX50SP2.xml"); + final quickfix.fix50sp2.QuoteRequest validatedMessage = (quickfix.fix50sp2.QuoteRequest) messageFactory.create(FixVersions.FIX50SP2, QuoteRequest.MSGTYPE); + validatedMessage.fromString(sourceFIXString, fix50sp2DataDictionary, true); + + String validateFIXString = validatedMessage.toString(); assertEquals("Message validation failed", sourceFIXString, validateFIXString); assertEquals(2, validatedMessage.getGroupCount(gNoRelatedSym.getFieldTag())); } - public void testValidationWithNestedGroupAndStandardFieldsWithoutDelimiter() { + @Test + public void testValidationWithNestedGroupAndStandardFieldsWithoutDelimiter() throws InvalidMessage { final quickfix.fix44.QuoteRequest quoteRequest = new quickfix.fix44.QuoteRequest(); final quickfix.field.QuoteReqID gQuoteReqID = new quickfix.field.QuoteReqID(); @@ -358,19 +367,15 @@ public void testValidationWithNestedGroupAndStandardFieldsWithoutDelimiter() { final quickfix.fix44.QuoteRequest.NoRelatedSym gNoRelatedSym = buildNestedGroupWithStandardFields("DEFAULT_VALUE"); quoteRequest.addGroup(gNoRelatedSym); - quoteRequest.addGroup(gNoRelatedSym); final String sourceFIXString = quoteRequest.toString(); - try { - buildValidatedMessage(sourceFIXString, defaultDataDictionary); - fail("No Exception thrown"); - } catch (final InvalidMessage e) { - // We expect that Exception did happen, so we don't do anything. - } + Message buildValidatedMessage = buildValidatedMessage(sourceFIXString, defaultDataDictionary); + assertEquals("The group 146 must set the delimiter field 55", buildValidatedMessage.getException().getMessage()); } + @Test public void testGroupFieldsOrderWithCustomDataDictionary() throws InvalidMessage { final quickfix.fix44.QuoteRequest quoteRequest = new quickfix.fix44.QuoteRequest(); @@ -405,15 +410,12 @@ public void testGroupFieldsOrderWithCustomDataDictionary() throws InvalidMessage assertNull("Invalid message", validatedMessage.getException()); - String validatedFIXString = null; - if (validatedMessage != null) { - validatedFIXString = validatedMessage.toString(); - } - + String validatedFIXString = validatedMessage.toString(); assertEquals("Message validation failed", MessageUtils.checksum(sourceFIXString), MessageUtils.checksum(validatedFIXString)); } + @Test public void testOutOfOrderGroupMembersDelimiterField() throws Exception { final Message m = new Message( "8=FIX.4.4\0019=0\00135=D\00134=2\00149=TW\00152=