diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5fa658cdf7..6efa4cea09 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,3 +6,6 @@ contact_links: - name: Stack Overflow url: https://stackoverflow.com/questions/ask?tags=quickfixj about: Please ask usage releated questions here and tag your question with "quickfixj". + - name: Discussions + url: https://github.com/quickfix-j/quickfixj/discussions + about: Please ask usage related questions here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..3f5555c833 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "maven" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" + open-pull-requests-limit: 10 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every weekday + interval: "daily" + open-pull-requests-limit: 10 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..330e5f5cc9 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,68 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# ******** NOTE ******** + +name: "CodeQL" + +on: + workflow_dispatch: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '39 6 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + #- name: Autobuild + # env: + # uses: github/codeql-action/autobuild@v2 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + - run: | + ./mvnw clean package -B -V -e -Pminimal-fix-latest -Dmaven.javadoc.skip=true -Dfindbugs.skip -Dcheckstyle.skip -Dpmd.skip=true -Dspotbugs.skip -Denforcer.skip -DskipTests -Dmaven.test.skip.exec -Dlicense.skip=true -Drat.skip=true -Dspotless.check.skip=true -D"java.util.logging.config.file"="logging.properties" -D"http.keepAlive"="false" -D"maven.wagon.http.pool"="false" -D"maven.wagon.httpconnectionManager.ttlSeconds"="120" + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000000..7fc23a3830 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,58 @@ +name: Java CI + +on: + workflow_dispatch: + push: + pull_request: + types: [reopened, opened, synchronize] + schedule: + - cron: '30 5 * * *' + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest] + java: [8, 11, 17] + fail-fast: false + max-parallel: 4 + name: Test JDK ${{ matrix.java }}, ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + cache: 'maven' + - name: Test with Maven + run: ./mvnw install -B -V -Pminimal-fix-latest -D"java.util.logging.config.file"="logging.properties" -D"http.keepAlive"="false" -D"maven.wagon.http.pool"="false" -D"maven.wagon.httpconnectionManager.ttlSeconds"="120" + + test-windows: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest] + java: [8, 11, 17] + fail-fast: false + max-parallel: 3 + name: Test JDK ${{ matrix.java }}, ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - name: Configure pagefile + uses: al-cheb/configure-pagefile-action@v1.3 + with: + minimum-size: 8GB + maximum-size: 16GB + disk-root: "C:" + - name: Set up Windows JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + cache: 'maven' + - name: Test with Maven on Windows + run: ./mvnw.cmd install -B -V -D"maven.javadoc.skip"="true" -P"skipBundlePlugin,minimal-fix-latest" -D"java.util.logging.config.file"="logging.properties" -D"http.keepAlive"="false" -D"maven.wagon.http.pool"="false" -D"maven.wagon.httpconnectionManager.ttlSeconds"="120" diff --git a/.gitignore b/.gitignore index f8f063ce17..536ba01d44 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ target/ *.iws # ignore NetBeans configuration nb-configuration.xml +*.bak diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000000..011b5a43c3 --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1 @@ +-Xms3g -Xmx6g -Djdk.xml.xpathExprGrpLimit=500 -Djdk.xml.xpathExprOpLimit=500 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..346d645fd0 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e0d98604d4..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: java -sudo: false -jdk: - - openjdk8 -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 2d404c0afe..3143d2f2b8 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,13 @@ QuickFIX/J ========== - -[![Build Status](https://travis-ci.com/quickfix-j/quickfixj.svg?branch=master)](https://travis-ci.com/quickfix-j/quickfixj) +[![Java CI](https://github.com/quickfix-j/quickfixj/actions/workflows/maven.yml/badge.svg)](https://github.com/quickfix-j/quickfixj/actions/workflows/maven.yml) +[![CodeQL](https://github.com/quickfix-j/quickfixj/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/quickfix-j/quickfixj/actions/workflows/codeql-analysis.yml) [![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) -[![Total alerts](https://img.shields.io/lgtm/alerts/g/quickfix-j/quickfixj.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/quickfix-j/quickfixj/alerts/) -[![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/quickfix-j/quickfixj.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/quickfix-j/quickfixj/context:java) This is the official QuickFIX/J project repository. ## intro -QuickFIX/J is a full featured messaging engine for the FIX protocol. +QuickFIX/J is a full featured messaging engine for the FIX protocol (FIX versions 4.0 - 5.0SP2/FIXT1.1 and FIXLatest). It is a 100% Java open source implementation of the popular C++ QuickFIX engine. The Financial Information eXchange (FIX) protocol is a messaging standard developed @@ -44,25 +42,45 @@ Pull requests are always welcome! Best is if you added a unit test to show that Fastest: clone the repo and issue the following command. ``` -$ mvn clean package -Dmaven.javadoc.skip=true -DskipTests -PskipBundlePlugin +$ mvnw clean package -Dmaven.javadoc.skip=true -DskipTests -PskipBundlePlugin,minimal-fix-latest ``` Slower: if you only want to skip the acceptance test suite: ``` -$ mvn clean package -Dmaven.javadoc.skip=true -DskipAT=true -PskipBundlePlugin +$ mvnw clean package -Dmaven.javadoc.skip=true -DskipAT=true -PskipBundlePlugin,minimal-fix-latest ``` Slow: if you want to run all tests: ``` -$ mvn clean package -Dmaven.javadoc.skip=true -PskipBundlePlugin +$ mvnw clean package -Dmaven.javadoc.skip=true -PskipBundlePlugin,minimal-fix-latest ``` NB: If you want to use the resulting JARs in an OSGi environment you'll have to omit the `-PskipBundlePlugin` option. +## importing the project into the IDE + +When the project is first created, it will not have the generated message classes and compile errors will occur! Best is to compile once on the command line before importing the project into the IDE. + +If the IDE reports some errors after the compilation with `mvnw clean package`, try to use `mvnw clean install`, like: +``` +$ mvnw clean install -Dmaven.javadoc.skip=true -DskipTests -PskipBundlePlugin,minimal-fix-latest +``` + ## configuration options https://rawgit.com/quickfix-j/quickfixj/master/quickfixj-core/src/main/doc/usermanual/usage/configuration.html ## basics +### example applications + +QuickFIX/J includes some example applications in the `quickfixj-examples` module. Moreover, here are some links to example applications: + +Examples by Geoffrey Gershaw: https://github.com/ggershaw/Examples + +Examples from QuickFIX/J Spring Boot Starter: https://github.com/esanchezros/quickfixj-spring-boot-starter-examples + +If you would like to be added to this list, please open a PR with the changes. + + ### Creating a QuickFIX/J application Implement the `quickfix.Application` interface. @@ -77,7 +95,7 @@ Here are explanations of what these functions provide for you. `onLogout` notifies you when an FIX session is no longer online. This could happen during a normal logout exchange or because of a forced termination or a loss of network connection. -`toAdmin` provides you with a peek at the administrative messages that are being sent from your FIX engine to the counter party. This is normally not useful for an application however it is provided for any logging you may wish to do. Notice that the `quickfix.Message` is mutable. This allows you to add fields before an adminstrative message before it is sent out. +`toAdmin` provides you with a peek at the administrative messages that are being sent from your FIX engine to the counter party. This is normally not useful for an application however it is provided for any logging you may wish to do. Notice that the `quickfix.Message` is mutable. This allows you to add fields to an administrative message before it is sent out. `toApp` is a callback for application messages that are being sent to a counterparty. If you throw a `DoNotSend` exception in this method, the application will not send the message. This is mostly useful if the application has been asked to resend a message such as an order that is no longer relevant for the current market. Messages that are being resent are marked with the `PossDupFlag` in the header set to true; If a `DoNotSend` exception is thrown and the flag is set to true, a sequence reset will be sent in place of the message. If it is set to false, the message will simply not be sent. Notice that the `quickfix.Message` is mutable. This allows you to add fields to an application message before it is sent out. @@ -265,3 +283,40 @@ void sendOrderCancelRequest() throws SessionNotFound { Session.sendToTarget(message, "TW", "TARGET"); } ``` + +## QuickFIX/J Runtime + +This project builds artefacts for the standard published FIX specification versions from FIX 4.0 to FIX Latest. + +* ```quickfixj-messages-fix40``` +* ```quickfixj-messages-fix41``` +* ```quickfixj-messages-fix42``` +* ```quickfixj-messages-fix43``` +* ```quickfixj-messages-fix44``` +* ```quickfixj-messages-fix50``` +* ```quickfixj-messages-fix50sp1``` +* ```quickfixj-messages-fix50sp2``` +* ```quickfixj-messages-fixlatest``` +* ```quickfixj-messages-fixt11``` +* ```quickfixj-messages-all``` - includes all of the above + +These artefacts are **test** dependencies of ```quickfixj-core```. They are **not** specified as _runtime_ dependencies, this makes it easier to customise QuickFIX/J deployments. + +If you have no need to customise a FIX integration then you can use the ```org.quickfixj``` artefacts built by this project. Simply include them as dependencies of your application. + +Artefacts for unused FIX specification versions can be omitted from your runtime. +Many integrations will not require ```quickfixj-messages-all``` and need only depend on artefacts for a subset of the FIX standard versions. Please note that FIX Protocol versions 5.0 and later depend on ```quickfixj-messages-fixt11``` which provides the implementation for the FIXT1.1 transport messages. + +Many integrations require specialisation of the FIX Messages, Components and/or Fields. This is accomplished by building and using custom artefacts. Please see [Customising QuickFIX/J](./customising-quickfixj.md) for more detail. + +### Application Dependencies for QuickFIX/J Messages Build + +![image info](./src/main/puml/dependencies_fixt11_fixlatest.png) + +![image info](./src/main/puml/dependencies_qfj_all.png) + +### Application Dependencies for Custom Messages Build + +![image info](./src/main/puml/custom_dependencies.png) + +![image info](./src/main/puml/custom_dependencies_fixt11_fixlatest.png) diff --git a/SECURITY.md b/SECURITY.md index 9d319cfe1e..867d215ffd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,8 +5,8 @@ | Version | Supported | | ------- | ------------------ | -| 2.1.x | :white_check_mark: | -| < 2.1.x | :x: | +| 2.3.x | :white_check_mark: | +| < 2.2.x | :x: | ## Reporting a Vulnerability diff --git a/customising-quickfixj.md b/customising-quickfixj.md new file mode 100644 index 0000000000..0bab497287 --- /dev/null +++ b/customising-quickfixj.md @@ -0,0 +1,94 @@ + +# Customising QuickFIX/J + +The core QuickFIX/J module is agnostic to FIX Protocol Versions. At runtime a QuickFIX/J dictionary with supporting implementation packages is required to use type-safe classes. + +The specification for a FIX integration is called a "Rules of Engagement". The Rules of Engagement can be customised with the mutual agreement of the respective counter-parties. + +The message, component and field implementations can be provided by a specialised build, along with the corresponding QuickFIX/J dictionary for the custom Rules of Engagement. + +The standard distribution of ```quickfixj-core``` can be used with custom artefacts. You need only build artefacts for versions of the Protocol that you use. These can be maintained independently from the QuickFIX/J project, while depending on the QuickFIX/J for the core functionality and tools. + +To build custom artefacts it's helpful to understand how QuickFIX/J builds the Field, Component and Message classes from the QuickFIX/J dictionaries and from [FIX Orchestra](https://www.fixtrading.org/standards/fix-orchestra/). + +The QuickFIX/J reference implementations for FIX versions FIX4.0 to FIX5.0sp2 and for FIXT1.1 are generated from the QuickFIX dictionaries for the specific version. The dictionaries are located in the ```src/main/resources``` directory of the respective modules of the ```quickfixj-messages``` module. +Maintaining the FIX4.0 to FIX5.0sp2 builds intentionally provides consistency with the prior QuickFIX/J 2 release in order to ease migration to QuickFIX/J 3. + +The most recent standard is defined as [FIX Latest](https://www.fixtrading.org/online-specification/). The QuickFIX/J reference implementation for FIX Latest is generated from a [FIX Orchestra](https://www.fixtrading.org/standards/fix-orchestra/) repository file. +An implementation or customisation of the FIX Standars derived from the FIX Orchestra repository is known as an "_orchestration_". +The standard FIX Orchestra repository requires some modification to work well with QuickFIX/J. +This is done by the ```quickfixj-orchestration``` module. +The ```quickfixj-orchestration``` module publishes a modified Orchestra artefact which can then be the basis of a custom FIX Latest build using QuickFIX/J . + +The complete reference FIX Latest specification results in a very large distribution. +To use FIX Latest, customisation of the [FIX Orchestra](https://www.fixtrading.org/standards/fix-orchestra/) repository is advisable. +Please see [QuickFIX/J Orchestration](./quickfixj-orchestration/readme.md) for details. + +## Customisation Scenarios + +### **Enable the use of ```BigDecimal``` for FIX Decimal Data Types** + +This behaviour is controlled by the ```${generator.decimal}``` build property. It is "false" by default to avoid surprising side effects of incompatible data types. + +To enable the use of ```BigDecimal``` in code generation, set the ```${generator.decimal}``` property to "true" in [quickfixj-messages](./quickfixj-messages/readme.md) and build the message artefacts. + +``` + + true + +``` +See [QuickFIX/J Messages](./quickfixj-messages/readme.md) for details of the build and recommendation for **how to implement custom builds.** + +### **Incompatible Data Types** + +Some incompatible changes have occurred in the evolution of the FIX protocol. For example see below changes to the type of **OrderQty (38)** : + +|FIX Version|Field Name|FIX Datatype|Base Type|QuickFIX/J Datatype| +|---|---|---|---|---| +|4.0|OrderQty|int|int|```int```| +|4.2|OrderQty|Qty|float|```Double``` or ```BigDecimal```| + +Only one ```quickfix.Field``` class with the same name may be loaded by the Java classloader so only one version of this Field should be in the classpath. QuickFix/J also verifies the data type using the supplied QuickFIX "Dictionary". + +Code generation using ```BigDecimal``` is incompatible at runtime with ```int``` for **OrderQty**. In this case, ```double``` is compatible with ```int``` at run time due to [widening primitive conversion](http://titanium.cs.berkeley.edu/doc/java-langspec-1.0/5.doc.html). + +Runtime incompatibilities can be resolved by: +* Amending the QuickFIX Dictionary to coerce the code generation and/or validation +* Changing the ordering of code generation and/or overwrite behaviour of code generation +* Omitting incompatible versions from your customised build +* Building artefacts independently for the conflicting versions and ensuring they are not used them in the same runtime + +See [QuickFIX/J Messages](./quickfixj-messages/readme.md) for details of the build and recommendation for **how to implement custom builds.** + +### **Customising the FIX Protocol for specialised Rules of Engagement** + +A Rules of Engagement can include customisation Messages, Components and Fields, including User Defined elements. + +It is not necessary to maintain a fork of the entire QuickFIX/J project to provide customised QuickFIX Dictionaries and to +generate type-safe libraries that are interoperable with QuickFIX/J. + +[FIX Orchestra](https://www.fixtrading.org/standards/fix-orchestra/) is intended for customisation to produce machine-readable Rules of Engagement. + +Consider creating a new project (or projects) to build the Messages, Components and Fields as needed for your specific Rules of Engagement. + +Edit the QuickFIX Dictionary or FIX Orchestra Repository (Orchestration) as required and +build the Messages, Components and Fields packages using the tools provided by the QuickFIX/J projects. + +QuickFIX/J Dictionaries, FIX Orchestra Orchestrations and/or documents can also be generated. + +See [QuickFIX/J Messages](./quickfixj-messages/readme.md) for details of the build and recommendation for **how to implement custom builds.** + +### **Managing incompatibility with Prior Versions of QuickFIX/J** + +From QuickFIX/J 3.0.0 the code generation for ```quickfix.Field``` prefers the FIX Orchestra Standard. This results in incompatible changes to the names of constants. + +For example : ```SettlType.REGULAR_FX_SPOT_SETTLEMENT``` becomes ```SettlType.REGULAR```. + +The required code changes may be trivial in most cases, but changes are elective. +The following describes how to use ```quickfixj-core``` from QuickFIX/J 3 without needing to implement code changes: +* build the required Message artefacts without the FIX Latest code generation. The Fields will then be generated only from legacy FIX Protocol Versions as they were prior to QuickFIX/J 3.0.0 - **or** +* if you want to use Messages, Components and/or Fields from FIX Latest while preferring legacy constants, +manipulate the order of code generation and/or the over-write behaviour of code behaviour to prefer earlier versions of FIX. +For example, generate FIX Latest first and overwrite the generated Field classes by subsequently running code generation for an earlier version. + +See [QuickFIX/J Messages](./quickfixj-messages/readme.md) for details of the build and recommendation for **how to implement custom builds.** diff --git a/mvnw b/mvnw new file mode 100755 index 0000000000..8d937f4c14 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000000..f80fbad3e7 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml index 470c2c5f98..8d0ff6336f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.quickfixj quickfixj-parent - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT pom QuickFIX/J Parent @@ -51,15 +51,14 @@ http://www.quickfixj.org/jira/ - - 3.5.0 - - quickfixj-codegenerator quickfixj-dictgenerator - quickfixj-core + quickfixj-class-pruner-maven-plugin + quickfixj-orchestration + quickfixj-base quickfixj-messages + quickfixj-core quickfixj-examples quickfixj-all quickfixj-distribution @@ -69,26 +68,234 @@ UTF-8 UTF-8 1.8 - 1.7.30 - 4.12 + 2.0.10 + 4.11.0 + 2.2 + 5.10.1 + 8 + 8 - - 3.1.0 - 3.8.0 - 3.1.0 - 2.22.0 - 3.10.0 - 3.0.1 - 2.10.4 - 3.1.1 - 3.1.0 - 3.5.1 - 1.6 - 2.8.2 - 1.6.8 - 3.0.0 + 3.8.7 + 3.9.6 + 3.9.6 + 3.3.1 + 3.12.1 + 3.3.0 + 3.2.3 + 3.21.2 + 3.3.0 + 3.6.3 + 3.5.1 + 3.6.0 + 5.1.9 + 3.1.0 + 3.1.1 + 1.6.13 + 3.5.0 + 3.4.2 + 3.0.0 + 3.10.2 + 3.3.0 + 1.2 + 8059 + 1.1.0 + 1.5.4 + 1.7.3 + 1.6.8 + 4.0.1 + 2.2.3 + 2.15.1 + OrchestraFIXLatest.xml + 1.0.2 + 0.9.1 + 2.0.0 + + + + io.fixprotocol.orchestrations + fix-standard + ${fix-orchestra.standard.version} + + + io.fixprotocol.orchestra + repository + ${fix-orchestra.repository.version} + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.junit.vintage + junit-vintage-engine + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.jupiter.version} + test + + + org.mockito + mockito-core + ${mockito-core.version} + test + + + org.hamcrest + hamcrest + ${hamcrest.version} + test + + + jakarta.xml.bind + jakarta.xml.bind-api + ${jaxb.version} + + + com.sun.xml.bind + jaxb-impl + runtime + ${jaxb.version} + + + org.apache.mina + mina-core + ${apache.mina.version} + + + commons-io + commons-io + ${commons.io.version} + + + org.apache.maven + maven-plugin-api + ${maven-plugin-api-version} + + + org.apache.maven.shared + maven-shared-utils + ${maven-shared-utils.version} + + + org.apache.maven.shared + file-management + ${file-management.version} + + + org.apache.maven + maven-core + ${maven-libs-version} + + + org.apache.maven + maven-artifact + ${maven-libs-version} + provided + + + org.apache.maven + maven-compat + ${maven-libs-version} + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${maven-plugin-annotations.version} + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + ${maven-plugin-testing-harness.version} + + + org.apache.maven + maven-project + provided + 2.2.1 + + + org.codehaus.plexus + plexus-utils + 4.0.0 + + + org.codehaus.plexus + plexus-xml + 4.0.3 + + + com.sleepycat + je + 18.3.12 + true + + + + org.dom4j + dom4j + 2.1.4 + true + + + com.cloudhopper.proxool + proxool + ${proxool.version} + true + + + + avalon-framework + avalon-framework-api + + + + commons-logging + commons-logging + + + + + com.cloudhopper.proxool + proxool-cglib + ${proxool.version} + true + + + + avalon-framework + avalon-framework-api + + + + commons-logging + commons-logging + + + + + + jaxen + jaxen + ${jaxen.version} + runtime + + + + org.slf4j + slf4j-jdk14 + ${slf4j.version} + + + + @@ -108,8 +315,7 @@ ${maven-compiler-plugin-version} true - ${jdkLevel} - ${jdkLevel} + true 2g 4g @@ -132,6 +338,11 @@ maven-surefire-plugin ${maven-surefire-plugin-version} + + org.apache.maven.plugins + maven-surefire-report-plugin + ${maven-surefire-plugin-version} + org.apache.maven.plugins maven-pmd-plugin @@ -165,8 +376,9 @@ jar - -Xdoclint:none - 3g + true + none + 5g @@ -223,18 +435,113 @@ + + org.codehaus.mojo + xml-maven-plugin + ${xml-maven-plugin-version} + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin-version} + + + net.sourceforge.plantuml + plantuml + ${plantuml-version} + + + com.github.jeluard + plantuml-maven-plugin + ${plantuml-maven-plugin-version} + + + net.sourceforge.plantuml + plantuml + + + + + maven-invoker-plugin + 3.6.0 + + + maven-plugin-plugin + 3.10.2 + + + maven-install-plugin + 3.1.1 + + + maven-clean-plugin + 3.3.2 + + + org.quickfixj.orchestra + quickfixj-from-fix-orchestra-code-generator-maven-plugin + ${org.quickfixj.orchestra.tools.version} + + + org.quickfixj.orchestra + quickfixj-from-fix-orchestra-dictionary-generator-maven-plugin + ${org.quickfixj.orchestra.tools.version} + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + - org.apache.felix maven-bundle-plugin + + + com.github.jeluard + plantuml-maven-plugin + + + ${basedir} + + **/*.puml + + + true + + + + net.sourceforge.plantuml + plantuml + ${plantuml-version} + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-versions + + enforce + + + + + ${maven.version} + + + + + + - skipBundlePlugin @@ -243,15 +550,15 @@ - - org.apache.felix - maven-bundle-plugin - - - - - - + + org.apache.felix + maven-bundle-plugin + + + + + + @@ -291,8 +598,16 @@ + + java-8-compilation + + [9,) + + + 8 + + - ossrh diff --git a/quickfixj-all/pom.xml b/quickfixj-all/pom.xml index 0bf5a0f7b8..f32414aad3 100644 --- a/quickfixj-all/pom.xml +++ b/quickfixj-all/pom.xml @@ -4,7 +4,7 @@ org.quickfixj quickfixj-parent - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT quickfixj-all @@ -21,22 +21,17 @@ org.quickfixj - quickfixj-core - ${project.version} - - - org.quickfixj - quickfixj-messages-all + quickfixj-base ${project.version} org.quickfixj - quickfixj-codegenerator + quickfixj-core ${project.version} org.quickfixj - quickfixj-dictgenerator + quickfixj-messages-all ${project.version} @@ -77,13 +72,12 @@ - + com.sleepycat*;resolution:=optional, org.apache.maven*;resolution:=optional, org.codehaus.plexus*;resolution:=optional, - org.logicalcobwebs.proxool*;resolution:=optional, + com.zaxxer*;resolution:=optional, org.dom4j*;resolution:=optional, * diff --git a/quickfixj-base/pom.xml b/quickfixj-base/pom.xml new file mode 100644 index 0000000000..4c2a7eb601 --- /dev/null +++ b/quickfixj-base/pom.xml @@ -0,0 +1,291 @@ + + 4.0.0 + + org.quickfixj + quickfixj-parent + 3.0.0-SNAPSHOT + + + quickfixj-base + jar + + QuickFIX/J Base + Base classes for messages and fields + http://www.quickfixj.org + + + + + + + org.junit.vintage + junit-vintage-engine + test + + + org.mockito + mockito-core + test + + + org.hamcrest + hamcrest + test + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + + + + + + ${project.basedir}/src/test/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-fix40/src/main/resources + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + quickfix/fixlatest/** + + + + + org.codehaus.mojo + xml-maven-plugin + + + extractRequiredFields + generate-sources + + transform + + + + + ${project.basedir}/../quickfixj-orchestration/target/generated-resources + ${project.build.directory}/generated-resources/extracted + + ${orchestra.file} + + ${project.basedir}/src/main/xsl/extractRequiredFields.xsl + + + + + + addRequiredFields + generate-sources + + transform + + + + + ${project.build.directory}/generated-resources/extracted + ${project.build.directory}/generated-resources + + ${orchestra.file} + + ${project.basedir}/src/main/xsl/addRequiredFields.xsl + + + + + + + + + org.quickfixj.orchestra + quickfixj-from-fix-orchestra-code-generator-maven-plugin + + + generate-sources + + codeGeneration + + fixLatest + + + + ${project.build.directory}/generated-resources/${orchestra.file} + ${project.build.directory}/generated-sources + + + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${project.build.directory}/generated-sources + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + quickfix/* + org/** + quickfix/field/* + quickfix/field/converter/* + + + quickfix/fixt11 + quickfix/fixlatest + + + + + org.apache.maven.plugins + maven-source-plugin + + + quickfix/* + quickfix/mina/* + org/** + quickfix/field/* + quickfix/field/converter/* + + + quickfix/fixt11 + quickfix/fixlatest + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + true + 8 + src/main/java:${project.build.directory}/generated-sources + quickfix.fixt11:quickfix.fixlatest + + + + + + org.apache.felix + maven-bundle-plugin + + + quickfix,quickfix.*,org.quickfixj,org.quickfixj.* + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + ${maven-surefire-plugin-version} + + + true + + + + maven-jxr-plugin + 3.3.1 + + + + + + + + surefire-java8 + + 1.8 + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + false + + + + + + + + + + + + + + surefire + + [1.9,) + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + false + + + + + + + + + + + + + diff --git a/quickfixj-base/readme.md b/quickfixj-base/readme.md new file mode 100644 index 0000000000..a70d37a5b3 --- /dev/null +++ b/quickfixj-base/readme.md @@ -0,0 +1,19 @@ +# quickfixj-base + +The ```quickfixj-base``` module consists of Java classes on which generated QuickFIX/J Fields, Components and Messages depend. + +There is a mutual dependency for a small number of generated Fields used in the ```Standard Header``` and ```Standard Trailer```. These Fields are therefore generated by this module, compiled by it and provided in the jar artefact. + +To assure Java runtime compatibility these Fields should not be included in other QuickFIX/J or custom artefacts. The list of fields can be found in the [xslt transform](./src/main/xsl/extractRequiredFields.xsl) used by the build in ```./src/main/xsl/```. + +The Fields in question are those defined in the (perhaps counter-intuitive) template match expression. + + Example: + +``` + DEFAULT_DOCUMENT_BUILDER_FACTORY_SUPPLIER = createDocumentBuilderFactorySupplier(); + + private static Supplier createDocumentBuilderFactorySupplier() { + return () -> { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + + if (JDK_DOCUMENT_BUILDER_FACTORY_NAME.equals(documentBuilderFactory.getClass().getName())) { + // disallow access to external DTD and schema when using JDK Xerces implementation + documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + } + + return documentBuilderFactory; + }; + } + private boolean hasVersion = false; private boolean checkFieldsOutOfOrder = true; private boolean checkFieldsHaveValues = true; @@ -75,6 +94,11 @@ public class DataDictionary { private boolean checkUnorderedGroupFields = true; private boolean allowUnknownMessageFields = false; private String beginString; + private String fullVersion; + private String majorVersion; + private int minorVersion; + private int extensionPack; + private int servicePack; private final Map> messageFields = new HashMap<>(); private final Map> requiredFields = new HashMap<>(); private final Set messages = new HashSet<>(); @@ -100,7 +124,19 @@ private DataDictionary() { * @throws ConfigError */ public DataDictionary(String location) throws ConfigError { - read(location); + this(location, DEFAULT_DOCUMENT_BUILDER_FACTORY_SUPPLIER); + } + + /** + * Initialize a data dictionary from a URL or a file path. + * + * @param location a URL or file system path + * @param documentBuilderFactorySupplier custom document builder factory supplier + * @throws ConfigError + */ + public DataDictionary(String location, Supplier documentBuilderFactorySupplier) throws + ConfigError { + read(location, documentBuilderFactorySupplier.get()); } /** @@ -110,7 +146,18 @@ public DataDictionary(String location) throws ConfigError { * @throws ConfigError */ public DataDictionary(InputStream in) throws ConfigError { - load(in); + this(in, DEFAULT_DOCUMENT_BUILDER_FACTORY_SUPPLIER); + } + + /** + * Initialize a data dictionary from an input stream. + * + * @param in the input stream + * @param documentBuilderFactorySupplier custom document builder factory supplier + * @throws ConfigError + */ + public DataDictionary(InputStream in, Supplier documentBuilderFactorySupplier) throws ConfigError { + load(in, documentBuilderFactorySupplier.get()); } /** @@ -128,14 +175,57 @@ private void setVersion(String beginString) { } /** - * Get the FIX version associated with this dictionary. - * + * Get the FIX major/minor version associated with this dictionary. + * E.g. FIX.5.0 * @return the FIX version */ public String getVersion() { return beginString; } + private void setFullVersion(String fullVersion) { + this.fullVersion = fullVersion; + } + + /** + * Get the FIX major/minor/SP/EP version associated with this dictionary. + * E.g. FIX.5.0.SP2_EP260 + * @return the full FIX version + */ + public String getFullVersion() { + return fullVersion; + } + + /** + * @return the ExtensionPack (EP), 0 if it is undefined. + */ + public int getExtensionPack() { + return extensionPack; + } + + /** + * @return the ServicePack (SP), 0 if it is undefined. + */ + public int getServicePack() { + return servicePack; + } + + /** + * @return the minor FIX version, 0 if it is undefined. + */ + public int getMinorVersion() { + return minorVersion; + } + + /** + * NOTE: this is of type String to cover the "Latest" case. + * + * @return the major FIX version + */ + public String getMajorVersion() { + return majorVersion; + } + private void addField(int field) { fields.add(field); } @@ -172,6 +262,17 @@ public String getValueName(int field, String value) { return valueNames.get(field, value); } + /** + * Get the value, if any, for an enumerated value name. + * + * @param field the tag + * @param name the value name + * @return the value assigned to passed name + */ + public String getValue(int field, String name) { + return valueNames.getValue(field, name); + } + /** * Predicate for determining if a tag is a defined field. * @@ -514,6 +615,11 @@ public void setAllowUnknownMessageFields(boolean allowUnknownFields) { private void copyFrom(DataDictionary rhs) { hasVersion = rhs.hasVersion; beginString = rhs.beginString; + fullVersion = rhs.fullVersion; + majorVersion = rhs.majorVersion; + minorVersion = rhs.minorVersion; + extensionPack = rhs.extensionPack; + servicePack = rhs.servicePack; copyMap(messageFields, rhs.messageFields); copyMap(requiredFields, rhs.requiredFields); @@ -644,9 +750,8 @@ private static boolean isVersionSpecified(DataDictionary dd) { private void iterate(FieldMap map, String msgType, DataDictionary dd) throws IncorrectTagValue, IncorrectDataFormat { - final Iterator> iterator = map.iterator(); - while (iterator.hasNext()) { - final StringField field = (StringField) iterator.next(); + for (final Field f : map) { + final StringField field = (StringField) f; checkHasValue(field); @@ -846,7 +951,7 @@ private int countElementNodes(NodeList nodes) { return elementNodesCount; } - private void read(String location) throws ConfigError { + private void read(String location, DocumentBuilderFactory factory) throws ConfigError { final InputStream inputStream = FileUtil.open(getClass(), location, URL, FILESYSTEM, CONTEXT_RESOURCE, CLASSLOADER_RESOURCE); if (inputStream == null) { @@ -854,7 +959,7 @@ private void read(String location) throws ConfigError { } try { - load(inputStream); + load(inputStream, factory); } catch (final Exception e) { throw new ConfigError(location + ": " + e.getMessage(), e); } finally { @@ -866,8 +971,7 @@ private void read(String location) throws ConfigError { } } - private void load(InputStream inputStream) throws ConfigError { - final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + private void load(InputStream inputStream, DocumentBuilderFactory factory) throws ConfigError { Document document; try { final DocumentBuilder builder = factory.newDocumentBuilder(); @@ -886,16 +990,35 @@ private void load(InputStream inputStream) throws ConfigError { throw new ConfigError("major attribute not found on "); } - if (!documentElement.hasAttribute("minor")) { - throw new ConfigError("minor attribute not found on "); - } + majorVersion = documentElement.getAttribute("major"); + minorVersion = getIntegerAttributeIfDefined(documentElement, "minor"); + servicePack = getIntegerAttributeIfDefined(documentElement, "servicepack"); + extensionPack = getIntegerAttributeIfDefined(documentElement, "extensionpack"); final String dictionaryType = documentElement.hasAttribute("type") ? documentElement .getAttribute("type") : FIX_PREFIX; - setVersion(dictionaryType + "." + documentElement.getAttribute("major") + "." - + documentElement.getAttribute("minor")); - + if (FixVersions.LATEST.equals(majorVersion)) { + String version = dictionaryType + "." + majorVersion; + setVersion(version); + String fullVersion = version; + if (extensionPack > 0) { + fullVersion = fullVersion + "_EP" + extensionPack; + } + setFullVersion(fullVersion); + } else { + String version = dictionaryType + "." + majorVersion + "." + minorVersion; + setVersion(version); + String fullVersion = version; + if (servicePack > 0) { + fullVersion = fullVersion + "SP" + servicePack; + } + if (extensionPack > 0) { + fullVersion = fullVersion + "_EP" + extensionPack; + } + setFullVersion(fullVersion); + } + // Index Components final NodeList componentsNode = documentElement.getElementsByTagName("components"); if (componentsNode.getLength() > 0) { @@ -1029,6 +1152,15 @@ private void load(InputStream inputStream) throws ConfigError { calculateOrderedFields(); } + private int getIntegerAttributeIfDefined(final Element documentElement, final String attribute) throws ConfigError { + try { + return documentElement.hasAttribute(attribute) + ? Integer.parseInt(documentElement.getAttribute(attribute)) : 0; + } catch (NumberFormatException e) { + throw new ConfigError("Attribute " + attribute + " could not be parsed as Integer.", e); + } + } + public int getNumMessageCategories() { return messageCategory.size(); } @@ -1036,8 +1168,9 @@ public int getNumMessageCategories() { private void load(Document document, String msgtype, Node node) throws ConfigError { String name; final NodeList fieldNodes = node.getChildNodes(); - if (countElementNodes(fieldNodes) == 0) { - throw new ConfigError("No fields found: msgType=" + msgtype); + + if (countElementNodes(fieldNodes) == 0 && (msgtype == HEADER_ID || msgtype == TRAILER_ID)) { + throw new ConfigError("No fields found in " + msgtype); } for (int j = 0; j < fieldNodes.getLength(); j++) { @@ -1255,6 +1388,18 @@ public V get(int field, String group) { return map == null ? null : map.get(group); } + public String getValue(int field, String name) { + Map map = get(field); + if (map != null) { + for (Entry entry : map.entrySet()) { + if (entry.getValue().equals(name)) { + return entry.getKey(); + } + } + } + return null; + } + public void put(int field, String group, V value) { computeIfAbsent(field, __ -> new HashMap<>()) .put(group, value); diff --git a/quickfixj-core/src/main/java/quickfix/DateField.java b/quickfixj-base/src/main/java/quickfix/DateField.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/DateField.java rename to quickfixj-base/src/main/java/quickfix/DateField.java diff --git a/quickfixj-core/src/main/java/quickfix/DayConverter.java b/quickfixj-base/src/main/java/quickfix/DayConverter.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/DayConverter.java rename to quickfixj-base/src/main/java/quickfix/DayConverter.java diff --git a/quickfixj-core/src/main/java/quickfix/DecimalField.java b/quickfixj-base/src/main/java/quickfix/DecimalField.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/DecimalField.java rename to quickfixj-base/src/main/java/quickfix/DecimalField.java diff --git a/quickfixj-core/src/main/java/quickfix/Dictionary.java b/quickfixj-base/src/main/java/quickfix/Dictionary.java similarity index 99% rename from quickfixj-core/src/main/java/quickfix/Dictionary.java rename to quickfixj-base/src/main/java/quickfix/Dictionary.java index ab7989b427..08da9d8378 100644 --- a/quickfixj-core/src/main/java/quickfix/Dictionary.java +++ b/quickfixj-base/src/main/java/quickfix/Dictionary.java @@ -25,7 +25,7 @@ /** * Name/value pairs used for specifying groups of settings. * - * @see SessionSettings + * see SessionSettings */ public class Dictionary { private String name; diff --git a/quickfixj-core/src/main/java/quickfix/DoNotSend.java b/quickfixj-base/src/main/java/quickfix/DoNotSend.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/DoNotSend.java rename to quickfixj-base/src/main/java/quickfix/DoNotSend.java diff --git a/quickfixj-core/src/main/java/quickfix/DoubleField.java b/quickfixj-base/src/main/java/quickfix/DoubleField.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/DoubleField.java rename to quickfixj-base/src/main/java/quickfix/DoubleField.java diff --git a/quickfixj-core/src/main/java/quickfix/Field.java b/quickfixj-base/src/main/java/quickfix/Field.java similarity index 98% rename from quickfixj-core/src/main/java/quickfix/Field.java rename to quickfixj-base/src/main/java/quickfix/Field.java index 07fde13be3..468905ae9f 100644 --- a/quickfixj-core/src/main/java/quickfix/Field.java +++ b/quickfixj-base/src/main/java/quickfix/Field.java @@ -80,10 +80,11 @@ public T getObject() { } /** - * Return's the formatted field (tag=value) + * Returns the formatted field (tag=value) * * @return the formatted field */ + @Override public String toString() { calculate(); return data; diff --git a/quickfixj-core/src/main/java/quickfix/FieldConvertError.java b/quickfixj-base/src/main/java/quickfix/FieldConvertError.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/FieldConvertError.java rename to quickfixj-base/src/main/java/quickfix/FieldConvertError.java diff --git a/quickfixj-core/src/main/java/quickfix/FieldException.java b/quickfixj-base/src/main/java/quickfix/FieldException.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/FieldException.java rename to quickfixj-base/src/main/java/quickfix/FieldException.java diff --git a/quickfixj-core/src/main/java/quickfix/FieldMap.java b/quickfixj-base/src/main/java/quickfix/FieldMap.java similarity index 96% rename from quickfixj-core/src/main/java/quickfix/FieldMap.java rename to quickfixj-base/src/main/java/quickfix/FieldMap.java index 834f00cd09..5ad5e21527 100644 --- a/quickfixj-core/src/main/java/quickfix/FieldMap.java +++ b/quickfixj-base/src/main/java/quickfix/FieldMap.java @@ -45,15 +45,15 @@ /** * Field container used by messages, groups, and composites. */ -public abstract class FieldMap implements Serializable { +public abstract class FieldMap implements Serializable, Iterable> { static final long serialVersionUID = -3193357271891865972L; private final int[] fieldOrder; - private final TreeMap> fields; + protected final TreeMap> fields; - private final TreeMap> groups = new TreeMap<>(); + protected final TreeMap> groups = new TreeMap<>(); /** * Constructs a FieldMap with the given field order. @@ -337,7 +337,7 @@ public void setField(int key, Field field) { public void setField(StringField field) { if (field.getValue() == null) { - throw new NullPointerException("Null field values are not allowed."); + throw new FieldException(SessionRejectReason.TAG_SPECIFIED_WITHOUT_A_VALUE, field.getField()); } fields.put(field.getField(), field); } @@ -448,6 +448,7 @@ public void removeField(int field) { fields.remove(field); } + @Override public Iterator> iterator() { return fields.values().iterator(); } @@ -601,8 +602,21 @@ public int getGroupCount(int tag) { return getGroups(tag).size(); } + /** + * @deprecated use {@linkplain #groupKeys()} instead + */ + @Deprecated public Iterator groupKeyIterator() { - return groups.keySet().iterator(); + return groupKeys().iterator(); + } + + /** + * Returns tags which are repeating group counters as {@code Iterable} + * + * @return tags which are repeating group counters + */ + public Iterable groupKeys() { + return groups.keySet(); } Map> getGroups() { @@ -669,7 +683,7 @@ public void replaceGroup(int num, Group group) { } public void removeGroup(int field) { - getGroups(field).clear(); + getGroups().remove(field); removeField(field); } @@ -707,5 +721,5 @@ public boolean hasGroup(Group group) { return hasGroup(group.getFieldTag()); } - + } diff --git a/quickfixj-core/src/main/java/quickfix/FieldNotFound.java b/quickfixj-base/src/main/java/quickfix/FieldNotFound.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/FieldNotFound.java rename to quickfixj-base/src/main/java/quickfix/FieldNotFound.java diff --git a/quickfixj-core/src/main/java/quickfix/FieldType.java b/quickfixj-base/src/main/java/quickfix/FieldType.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/FieldType.java rename to quickfixj-base/src/main/java/quickfix/FieldType.java diff --git a/quickfixj-core/src/main/java/quickfix/FileUtil.java b/quickfixj-base/src/main/java/quickfix/FileUtil.java similarity index 87% rename from quickfixj-core/src/main/java/quickfix/FileUtil.java rename to quickfixj-base/src/main/java/quickfix/FileUtil.java index c8159f7788..1ad20635e5 100644 --- a/quickfixj-core/src/main/java/quickfix/FileUtil.java +++ b/quickfixj-base/src/main/java/quickfix/FileUtil.java @@ -24,7 +24,11 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLConnection; + + public class FileUtil { public static String fileAppendPath(String pathPrefix, String pathSuffix) { @@ -141,7 +145,18 @@ public static InputStream open(Class clazz, String name, Location... location break; case URL: try { - in = new URL(name).openStream(); + URL url = new URL(name); + URLConnection urlConnection = url.openConnection(); + if (urlConnection instanceof HttpURLConnection) { + HttpURLConnection httpURLConnection = (HttpURLConnection)urlConnection; + httpURLConnection.setRequestProperty("User-Agent", "Java-QuickFIXJ-FileUtil"); + httpURLConnection.connect(); + in = httpURLConnection.getInputStream(); + } else { + if (urlConnection != null) { + in = urlConnection.getInputStream(); + } + } } catch (IOException e) { // ignore } diff --git a/quickfixj-core/src/main/java/quickfix/FixVersions.java b/quickfixj-base/src/main/java/quickfix/FixVersions.java similarity index 91% rename from quickfixj-core/src/main/java/quickfix/FixVersions.java rename to quickfixj-base/src/main/java/quickfix/FixVersions.java index 7cb7f98180..209d65baf8 100644 --- a/quickfixj-core/src/main/java/quickfix/FixVersions.java +++ b/quickfixj-base/src/main/java/quickfix/FixVersions.java @@ -29,12 +29,15 @@ public interface FixVersions { String BEGINSTRING_FIX43 = "FIX.4.3"; String BEGINSTRING_FIX44 = "FIX.4.4"; - /** - * FIX 5.0 does not have a begin string. + /* + * FIX 5.0+ does not have a begin string. */ String FIX50 = "FIX.5.0"; String FIX50SP1 = "FIX.5.0SP1"; String FIX50SP2 = "FIX.5.0SP2"; + + String LATEST = "Latest"; + String FIXLATEST = "FIX." + LATEST; // FIXT.x.x support diff --git a/quickfixj-core/src/main/java/quickfix/Group.java b/quickfixj-base/src/main/java/quickfix/Group.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/Group.java rename to quickfixj-base/src/main/java/quickfix/Group.java diff --git a/quickfixj-core/src/main/java/quickfix/HasFieldAndReason.java b/quickfixj-base/src/main/java/quickfix/HasFieldAndReason.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/HasFieldAndReason.java rename to quickfixj-base/src/main/java/quickfix/HasFieldAndReason.java diff --git a/quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java b/quickfixj-base/src/main/java/quickfix/IncorrectDataFormat.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/IncorrectDataFormat.java rename to quickfixj-base/src/main/java/quickfix/IncorrectDataFormat.java diff --git a/quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java b/quickfixj-base/src/main/java/quickfix/IncorrectTagValue.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/IncorrectTagValue.java rename to quickfixj-base/src/main/java/quickfix/IncorrectTagValue.java diff --git a/quickfixj-core/src/main/java/quickfix/IntField.java b/quickfixj-base/src/main/java/quickfix/IntField.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/IntField.java rename to quickfixj-base/src/main/java/quickfix/IntField.java diff --git a/quickfixj-core/src/main/java/quickfix/InvalidMessage.java b/quickfixj-base/src/main/java/quickfix/InvalidMessage.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/InvalidMessage.java rename to quickfixj-base/src/main/java/quickfix/InvalidMessage.java diff --git a/quickfixj-core/src/main/java/quickfix/Message.java b/quickfixj-base/src/main/java/quickfix/Message.java similarity index 92% rename from quickfixj-core/src/main/java/quickfix/Message.java rename to quickfixj-base/src/main/java/quickfix/Message.java index e5e336ec9b..c974021e03 100644 --- a/quickfixj-core/src/main/java/quickfix/Message.java +++ b/quickfixj-base/src/main/java/quickfix/Message.java @@ -66,7 +66,6 @@ import javax.xml.transform.stream.StreamResult; import java.io.ByteArrayOutputStream; import java.text.DecimalFormat; -import java.util.Iterator; import java.util.List; /** @@ -306,7 +305,7 @@ public boolean trailerHasGroup(Group group) { /** * Converts the message into a simple XML format. This format is - * probably not sufficient for production use, but it more intended + * probably not sufficient for production use, but is more intended * for diagnostics and debugging. THIS IS NOT FIXML. * * To get names instead of tag number, use toXML(DataDictionary) @@ -316,20 +315,52 @@ public boolean trailerHasGroup(Group group) { * @see #toXML(DataDictionary) */ public String toXML() { - return toXML(null); + return toXML(false); } /** * Converts the message into a simple XML format. This format is - * probably not sufficient for production use, but it more intended + * probably not sufficient for production use, but is more intended + * for diagnostics and debugging. THIS IS NOT FIXML. + * + * To get names instead of tag number, use toXML(DataDictionary, boolean) + * instead. + * + * @param indent specifies whether the Transformer may add additional + * whitespace when outputting the result tree + * @return an XML representation of the message. + * @see #toXML(DataDictionary, boolean) + */ + public String toXML(boolean indent) { + return toXML(null, indent); + } + + /** + * Converts the message into a simple XML format. This format is + * probably not sufficient for production use, but is more intended * for diagnostics and debugging. THIS IS NOT FIXML. * * @param dataDictionary * @return the XML representation of the message */ public String toXML(DataDictionary dataDictionary) { + return toXML(dataDictionary, false); + } + + /** + * Converts the message into a simple XML format. This format is + * probably not sufficient for production use, but is more intended + * for diagnostics and debugging. THIS IS NOT FIXML. + * + * @param indent specifies whether the Transformer may add additional + * whitespace when outputting the result tree + * @param dataDictionary + * @return the XML representation of the message + */ + public String toXML(DataDictionary dataDictionary, boolean indent) { try { - final Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder() + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + final Document document = factory.newDocumentBuilder() .newDocument(); final Element message = document.createElement("message"); document.appendChild(message); @@ -342,7 +373,12 @@ public String toXML(DataDictionary dataDictionary) { final TransformerFactory tf = TransformerFactory.newInstance(); final Transformer serializer = tf.newTransformer(); serializer.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1"); - serializer.setOutputProperty(OutputKeys.INDENT, "yes"); + if (indent) { + serializer.setOutputProperty(OutputKeys.INDENT, "yes"); + serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); + } else { + serializer.setOutputProperty(OutputKeys.INDENT, "no"); + } serializer.transform(domSource, streamResult); return out.toString(); } catch (final Exception e) { @@ -351,13 +387,11 @@ public String toXML(DataDictionary dataDictionary) { } private void toXMLFields(Element message, String section, FieldMap fieldMap, - DataDictionary dataDictionary) throws FieldNotFound { + DataDictionary dataDictionary) { final Document document = message.getOwnerDocument(); final Element fields = document.createElement(section); message.appendChild(fields); - final Iterator> fieldItr = fieldMap.iterator(); - while (fieldItr.hasNext()) { - final Field field = fieldItr.next(); + for (final Field field : fieldMap) { final Element fieldElement = document.createElement("field"); if (dataDictionary != null) { final String name = dataDictionary.getFieldName(field.getTag()); @@ -375,9 +409,7 @@ private void toXMLFields(Element message, String section, FieldMap fieldMap, fieldElement.appendChild(value); fields.appendChild(fieldElement); } - final Iterator groupKeyItr = fieldMap.groupKeyIterator(); - while (groupKeyItr.hasNext()) { - final int groupKey = groupKeyItr.next(); + for (final int groupKey : fieldMap.groupKeys()) { final Element groupsElement = document.createElement("groups"); fields.appendChild(groupsElement); if (dataDictionary != null) { @@ -572,7 +604,7 @@ void parse(String messageData, DataDictionary sessionDataDictionary, try { parseHeader(sessionDataDictionary, doValidation); - parseBody(applicationDataDictionary, doValidation); + parseBody(sessionDataDictionary, applicationDataDictionary, doValidation); parseTrailer(sessionDataDictionary); if (doValidation && validateChecksum) { validateCheckSum(messageData); @@ -638,34 +670,34 @@ private String getMsgType() throws InvalidMessage { } } - private void parseBody(DataDictionary dd, boolean doValidation) throws InvalidMessage { - StringField field = extractField(dd, this); + private void parseBody(DataDictionary sessionDataDictionary, DataDictionary applicationDataDictionary, boolean doValidation) throws InvalidMessage { + StringField field = extractField(applicationDataDictionary, this); while (field != null) { if (isTrailerField(field.getField())) { pushBack(field); return; } - if (isHeaderField(field.getField())) { + if (isHeaderField(field, sessionDataDictionary)) { // An acceptance test requires the sequence number to // be available even if the related field is out of order setField(header, field); // Group case - if (dd != null && dd.isGroup(DataDictionary.HEADER_ID, field.getField())) { - parseGroup(DataDictionary.HEADER_ID, field, dd, dd, header, doValidation); + if (sessionDataDictionary != null && sessionDataDictionary.isGroup(DataDictionary.HEADER_ID, field.getField())) { + parseGroup(DataDictionary.HEADER_ID, field, sessionDataDictionary, sessionDataDictionary, header, doValidation); } - if (doValidation && dd != null && dd.isCheckFieldsOutOfOrder()) + if (doValidation && sessionDataDictionary != null && sessionDataDictionary.isCheckFieldsOutOfOrder()) throw new FieldException(SessionRejectReason.TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER, field.getTag()); } else { setField(this, field); // Group case - if (dd != null && dd.isGroup(getMsgType(), field.getField())) { - parseGroup(getMsgType(), field, dd, dd, this, doValidation); + if (applicationDataDictionary != null && applicationDataDictionary.isGroup(getMsgType(), field.getField())) { + parseGroup(getMsgType(), field, applicationDataDictionary, applicationDataDictionary, this, doValidation); } } - field = extractField(dd, this); + field = extractField(applicationDataDictionary, this); } } @@ -738,7 +770,7 @@ private void parseGroup(String msgType, StringField field, DataDictionary dd, Da } } else { // QFJ-169/QFJ-791: handle unknown repeating group fields in the body - if (!isTrailerField(tag) && !(DataDictionary.HEADER_ID.equals(msgType))) { + if (!isTrailerField(tag) && !(DataDictionary.HEADER_ID.equals(msgType) || isHeaderField(field, dd))) { if (checkFieldValidation(parent, parentDD, field, msgType, doValidation, group)) { continue; } diff --git a/quickfixj-core/src/main/java/quickfix/MessageComponent.java b/quickfixj-base/src/main/java/quickfix/MessageComponent.java similarity index 58% rename from quickfixj-core/src/main/java/quickfix/MessageComponent.java rename to quickfixj-base/src/main/java/quickfix/MessageComponent.java index f172764dae..660ea41534 100644 --- a/quickfixj-core/src/main/java/quickfix/MessageComponent.java +++ b/quickfixj-base/src/main/java/quickfix/MessageComponent.java @@ -17,6 +17,12 @@ protected MessageComponent(int[] fieldOrder) { super(fieldOrder); } + /** + * Copies fields defined in the data dictionary inside this message component from specified source fields. This + * method is not symmetric with {@link MessageComponent#copyTo(FieldMap)} method. + * + * @param fields source fields + */ public void copyFrom(FieldMap fields) { try { for (int componentField : getFields()) { @@ -35,22 +41,23 @@ public void copyFrom(FieldMap fields) { } } + /** + * Copies all fields inside this message component to specified destination fields. This method is not symmetric + * with {@link MessageComponent#copyFrom(FieldMap)} method. + * + * @param fields destination fields + */ public void copyTo(FieldMap fields) { try { - for (int componentField : getFields()) { - if (isSetField(componentField)) { - fields.setField(componentField, getField(componentField)); - } + for (int componentField : this.fields.keySet()) { + fields.setField(componentField, getField(componentField)); } - for (int groupField : getGroupFields()) { - if (isSetField(groupField)) { - fields.setField(groupField, getField(groupField)); - fields.setGroups(groupField, getGroups(groupField)); - } + for (int groupField : this.groups.keySet()) { + fields.setField(groupField, getField(groupField)); + fields.setGroups(groupField, getGroups(groupField)); } } catch (FieldNotFound e) { // should not happen } } - } diff --git a/quickfixj-core/src/main/java/quickfix/MessageFactory.java b/quickfixj-base/src/main/java/quickfix/MessageFactory.java similarity index 91% rename from quickfixj-core/src/main/java/quickfix/MessageFactory.java rename to quickfixj-base/src/main/java/quickfix/MessageFactory.java index cf734f8313..dfab26f46a 100644 --- a/quickfixj-core/src/main/java/quickfix/MessageFactory.java +++ b/quickfixj-base/src/main/java/quickfix/MessageFactory.java @@ -24,7 +24,7 @@ /** * Used by a Session to create a Message. * - * @see quickfix.Session + * see quickfix.Session */ public interface MessageFactory { @@ -53,9 +53,9 @@ default Message create(String beginString, ApplVerID applVerID, String msgType) * Creates a group for the specified parent message type and * for the fields with the corresponding field ID * - * Example: to create a {@link quickfix.fix42.MarketDataRequest.NoMDEntryTypes} + * Example: to create a {quickfix.fix42.MarketDataRequest.NoMDEntryTypes} * you need to call - * create({@link quickfix.field.MsgType#MARKET_DATA_REQUEST}, {@link quickfix.field.NoMDEntryTypes#FIELD}) + * create({quickfix.field.MsgType#MARKET_DATA_REQUEST}, {quickfix.field.NoMDEntryTypes#FIELD}) * * Function returns null if the group cannot be created. * diff --git a/quickfixj-core/src/main/java/quickfix/MessageParseError.java b/quickfixj-base/src/main/java/quickfix/MessageParseError.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/MessageParseError.java rename to quickfixj-base/src/main/java/quickfix/MessageParseError.java diff --git a/quickfixj-core/src/main/java/quickfix/MessageUtils.java b/quickfixj-base/src/main/java/quickfix/MessageUtils.java similarity index 83% rename from quickfixj-core/src/main/java/quickfix/MessageUtils.java rename to quickfixj-base/src/main/java/quickfix/MessageUtils.java index 60f7c8bdbc..fe7a51b038 100644 --- a/quickfixj-core/src/main/java/quickfix/MessageUtils.java +++ b/quickfixj-base/src/main/java/quickfix/MessageUtils.java @@ -24,7 +24,6 @@ import quickfix.Message.Header; import quickfix.field.ApplVerID; import quickfix.field.BeginString; -import quickfix.field.DefaultApplVerID; import quickfix.field.MsgType; import quickfix.field.SenderCompID; import quickfix.field.SenderLocationID; @@ -118,76 +117,6 @@ public static Message parse(MessageFactory messageFactory, DataDictionary dataDi return message; } - /** - * NOTE: This method is intended for internal use. - * - * @param session the Session that will process the message - * @param messageString - * @return the parsed message - * @throws InvalidMessage - */ - public static Message parse(Session session, String messageString) throws InvalidMessage { - final String beginString = getStringField(messageString, BeginString.FIELD); - final String msgType = getMessageType(messageString); - - ApplVerID applVerID; - - if (FixVersions.BEGINSTRING_FIXT11.equals(beginString)) { - applVerID = getApplVerID(session, messageString); - } else { - applVerID = toApplVerID(beginString); - } - - final MessageFactory messageFactory = session.getMessageFactory(); - - final DataDictionaryProvider ddProvider = session.getDataDictionaryProvider(); - final DataDictionary sessionDataDictionary = ddProvider == null ? null : ddProvider - .getSessionDataDictionary(beginString); - final DataDictionary applicationDataDictionary = ddProvider == null ? null : ddProvider - .getApplicationDataDictionary(applVerID); - - final quickfix.Message message = messageFactory.create(beginString, applVerID, msgType); - final DataDictionary payloadDictionary = MessageUtils.isAdminMessage(msgType) - ? sessionDataDictionary - : applicationDataDictionary; - - final boolean doValidation = payloadDictionary != null; - final boolean validateChecksum = session.isValidateChecksum(); - - message.parse(messageString, sessionDataDictionary, payloadDictionary, doValidation, - validateChecksum); - - return message; - } - - private static ApplVerID getApplVerID(Session session, String messageString) - throws InvalidMessage { - ApplVerID applVerID = null; - - final String applVerIdString = getStringField(messageString, ApplVerID.FIELD); - if (applVerIdString != null) { - applVerID = new ApplVerID(applVerIdString); - } - - if (applVerID == null) { - applVerID = session.getTargetDefaultApplicationVersionID(); - } - - if (applVerID == null && isLogon(messageString)) { - final String defaultApplVerIdString = getStringField(messageString, - DefaultApplVerID.FIELD); - if (defaultApplVerIdString != null) { - applVerID = new ApplVerID(defaultApplVerIdString); - } - } - - if (applVerID == null) { - throw newInvalidMessageException("Can't determine ApplVerID from message " + messageString, getMinimalMessage(messageString)); - } - - return applVerID; - } - public static boolean isAdminMessage(String msgType) { return msgType.length() == 1 && "0A12345".contains(msgType); } @@ -274,6 +203,7 @@ public static String getStringField(String messageString, int tag) { put(ApplVerID.FIX50, FixVersions.FIX50); put(ApplVerID.FIX50SP1, FixVersions.FIX50SP1); put(ApplVerID.FIX50SP2, FixVersions.FIX50SP2); + put(ApplVerID.FIXLATEST, FixVersions.FIXLATEST); } }; @@ -304,6 +234,7 @@ public static String toBeginString(ApplVerID applVerID) throws QFJException { put(FixVersions.FIX50, new ApplVerID(ApplVerID.FIX50)); put(FixVersions.FIX50SP1, new ApplVerID(ApplVerID.FIX50SP1)); put(FixVersions.FIX50SP2, new ApplVerID(ApplVerID.FIX50SP2)); + put(FixVersions.FIXLATEST, new ApplVerID(ApplVerID.FIXLATEST)); } }; diff --git a/quickfixj-core/src/main/java/quickfix/NumbersCache.java b/quickfixj-base/src/main/java/quickfix/NumbersCache.java similarity index 85% rename from quickfixj-core/src/main/java/quickfix/NumbersCache.java rename to quickfixj-base/src/main/java/quickfix/NumbersCache.java index fdacaa695b..4460c0bc75 100644 --- a/quickfixj-core/src/main/java/quickfix/NumbersCache.java +++ b/quickfixj-base/src/main/java/quickfix/NumbersCache.java @@ -32,19 +32,21 @@ public final class NumbersCache { static { LITTLE_NUMBERS = new ArrayList<>(LITTLE_NUMBERS_LENGTH); - for (int i = 0; i < LITTLE_NUMBERS_LENGTH; i++) + 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 + * @param i the int to convert + * @return the String representing the integer */ public static String get(int i) { - if (i < LITTLE_NUMBERS_LENGTH) + if (i < LITTLE_NUMBERS_LENGTH && i >= 0) { return LITTLE_NUMBERS.get(i); + } return String.valueOf(i); } @@ -56,10 +58,10 @@ public static String get(int i) { * @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) + long l = (long) d; + if (d == (double) l) { return get(l); + } return null; - } - + } } diff --git a/quickfixj-core/src/main/java/quickfix/RuntimeError.java b/quickfixj-base/src/main/java/quickfix/RuntimeError.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/RuntimeError.java rename to quickfixj-base/src/main/java/quickfix/RuntimeError.java diff --git a/quickfixj-core/src/main/java/quickfix/SessionID.java b/quickfixj-base/src/main/java/quickfix/SessionID.java similarity index 99% rename from quickfixj-core/src/main/java/quickfix/SessionID.java rename to quickfixj-base/src/main/java/quickfix/SessionID.java index 59c9d875d2..2d4f6dae2d 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionID.java +++ b/quickfixj-base/src/main/java/quickfix/SessionID.java @@ -163,7 +163,7 @@ public String getSessionQualifier() { @Override public boolean equals(Object object) { if (!(object instanceof SessionID)) { - return false; + return false; } return toString().equals(object.toString()); } diff --git a/quickfixj-core/src/main/java/quickfix/SessionRejectReasonText.java b/quickfixj-base/src/main/java/quickfix/SessionRejectReasonText.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/SessionRejectReasonText.java rename to quickfixj-base/src/main/java/quickfix/SessionRejectReasonText.java diff --git a/quickfixj-core/src/main/java/quickfix/StringField.java b/quickfixj-base/src/main/java/quickfix/StringField.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/StringField.java rename to quickfixj-base/src/main/java/quickfix/StringField.java diff --git a/quickfixj-core/src/main/java/quickfix/SystemTime.java b/quickfixj-base/src/main/java/quickfix/SystemTime.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/SystemTime.java rename to quickfixj-base/src/main/java/quickfix/SystemTime.java diff --git a/quickfixj-core/src/main/java/quickfix/SystemTimeSource.java b/quickfixj-base/src/main/java/quickfix/SystemTimeSource.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/SystemTimeSource.java rename to quickfixj-base/src/main/java/quickfix/SystemTimeSource.java diff --git a/quickfixj-core/src/main/java/quickfix/UnsupportedMessageType.java b/quickfixj-base/src/main/java/quickfix/UnsupportedMessageType.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/UnsupportedMessageType.java rename to quickfixj-base/src/main/java/quickfix/UnsupportedMessageType.java diff --git a/quickfixj-core/src/main/java/quickfix/UnsupportedVersion.java b/quickfixj-base/src/main/java/quickfix/UnsupportedVersion.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/UnsupportedVersion.java rename to quickfixj-base/src/main/java/quickfix/UnsupportedVersion.java diff --git a/quickfixj-core/src/main/java/quickfix/UtcDateField.java b/quickfixj-base/src/main/java/quickfix/UtcDateField.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/UtcDateField.java rename to quickfixj-base/src/main/java/quickfix/UtcDateField.java diff --git a/quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java b/quickfixj-base/src/main/java/quickfix/UtcDateOnlyField.java similarity index 99% rename from quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java rename to quickfixj-base/src/main/java/quickfix/UtcDateOnlyField.java index cc26ab9fb9..ced3aeb2f8 100644 --- a/quickfixj-core/src/main/java/quickfix/UtcDateOnlyField.java +++ b/quickfixj-base/src/main/java/quickfix/UtcDateOnlyField.java @@ -34,7 +34,7 @@ public UtcDateOnlyField(int field) { protected UtcDateOnlyField(int field, LocalDate data) { super(field, data); } - + public void setValue(LocalDate value) { setObject(value); } diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimeField.java b/quickfixj-base/src/main/java/quickfix/UtcTimeField.java similarity index 89% rename from quickfixj-core/src/main/java/quickfix/UtcTimeField.java rename to quickfixj-base/src/main/java/quickfix/UtcTimeField.java index 52096d446e..98e991cf3c 100644 --- a/quickfixj-core/src/main/java/quickfix/UtcTimeField.java +++ b/quickfixj-base/src/main/java/quickfix/UtcTimeField.java @@ -27,7 +27,7 @@ */ public class UtcTimeField extends Field { - protected UtcTimestampPrecision precision = UtcTimestampPrecision.MILLIS; + protected UtcTimestampPrecision precision = getDefaultUtcTimestampPrecision(); protected UtcTimeField(int field) { super(field, LocalTime.now(ZoneOffset.UTC)); @@ -57,4 +57,8 @@ public boolean valueEquals(LocalTime value) { public UtcTimestampPrecision getPrecision() { return precision; } + + protected UtcTimestampPrecision getDefaultUtcTimestampPrecision() { + return UtcTimestampPrecision.MILLIS; + } } diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java b/quickfixj-base/src/main/java/quickfix/UtcTimeOnlyField.java similarity index 91% rename from quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java rename to quickfixj-base/src/main/java/quickfix/UtcTimeOnlyField.java index d45e201cdd..c41206d9b1 100644 --- a/quickfixj-core/src/main/java/quickfix/UtcTimeOnlyField.java +++ b/quickfixj-base/src/main/java/quickfix/UtcTimeOnlyField.java @@ -27,7 +27,7 @@ */ public class UtcTimeOnlyField extends Field { - private UtcTimestampPrecision precision = UtcTimestampPrecision.MILLIS; + private UtcTimestampPrecision precision = getDefaultUtcTimestampPrecision(); public UtcTimeOnlyField(int field) { super(field, LocalTime.now(ZoneOffset.UTC)); @@ -68,4 +68,7 @@ public boolean valueEquals(LocalTime value) { return getValue().equals(value); } + protected UtcTimestampPrecision getDefaultUtcTimestampPrecision() { + return UtcTimestampPrecision.MILLIS; + } } diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java b/quickfixj-base/src/main/java/quickfix/UtcTimeStampField.java similarity index 92% rename from quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java rename to quickfixj-base/src/main/java/quickfix/UtcTimeStampField.java index e100166730..14cfd5ff3b 100644 --- a/quickfixj-core/src/main/java/quickfix/UtcTimeStampField.java +++ b/quickfixj-base/src/main/java/quickfix/UtcTimeStampField.java @@ -27,7 +27,7 @@ */ public class UtcTimeStampField extends Field { - private UtcTimestampPrecision precision = UtcTimestampPrecision.MILLIS; + private UtcTimestampPrecision precision = getDefaultUtcTimestampPrecision(); public UtcTimeStampField(int field) { super(field, LocalDateTime.now(ZoneOffset.UTC)); @@ -73,4 +73,7 @@ public boolean valueEquals(LocalDateTime value) { return getValue().equals(value); } + protected UtcTimestampPrecision getDefaultUtcTimestampPrecision() { + return UtcTimestampPrecision.MILLIS; + } } diff --git a/quickfixj-core/src/main/java/quickfix/UtcTimestampPrecision.java b/quickfixj-base/src/main/java/quickfix/UtcTimestampPrecision.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/UtcTimestampPrecision.java rename to quickfixj-base/src/main/java/quickfix/UtcTimestampPrecision.java diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java b/quickfixj-base/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java similarity index 91% rename from quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java rename to quickfixj-base/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java index c19e85a4dd..b83ca5675e 100644 --- a/quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java +++ b/quickfixj-base/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java @@ -47,7 +47,7 @@ protected static void assertLength(String value, String type, int... lengths) th protected static void assertDigitSequence(String value, int i, int j, String type) throws FieldConvertError { for (int offset = i; offset < j; offset++) { - if (!Character.isDigit(value.charAt(offset))) { + if (!IntConverter.isDigit(value.charAt(offset))) { throwFieldConvertError(value, type); } } @@ -65,14 +65,6 @@ protected static void throwFieldConvertError(String value, String type) throw new FieldConvertError("invalid UTC " + type + " value: " + value); } - protected static long parseLong(String s) { - long n = 0; - for (int i = 0; i < s.length(); i++) { - n = (n * 10) + (s.charAt(i) - '0'); - } - return n; - } - protected DateFormat createDateFormat(String format) { SimpleDateFormat sdf = new SimpleDateFormat(format); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/BooleanConverter.java b/quickfixj-base/src/main/java/quickfix/field/converter/BooleanConverter.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/field/converter/BooleanConverter.java rename to quickfixj-base/src/main/java/quickfix/field/converter/BooleanConverter.java diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/CharArrayConverter.java b/quickfixj-base/src/main/java/quickfix/field/converter/CharArrayConverter.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/field/converter/CharArrayConverter.java rename to quickfixj-base/src/main/java/quickfix/field/converter/CharArrayConverter.java diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/CharConverter.java b/quickfixj-base/src/main/java/quickfix/field/converter/CharConverter.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/field/converter/CharConverter.java rename to quickfixj-base/src/main/java/quickfix/field/converter/CharConverter.java diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/DecimalConverter.java b/quickfixj-base/src/main/java/quickfix/field/converter/DecimalConverter.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/field/converter/DecimalConverter.java rename to quickfixj-base/src/main/java/quickfix/field/converter/DecimalConverter.java diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/DoubleConverter.java b/quickfixj-base/src/main/java/quickfix/field/converter/DoubleConverter.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/field/converter/DoubleConverter.java rename to quickfixj-base/src/main/java/quickfix/field/converter/DoubleConverter.java diff --git a/quickfixj-base/src/main/java/quickfix/field/converter/IntConverter.java b/quickfixj-base/src/main/java/quickfix/field/converter/IntConverter.java new file mode 100644 index 0000000000..21f04b28e5 --- /dev/null +++ b/quickfixj-base/src/main/java/quickfix/field/converter/IntConverter.java @@ -0,0 +1,169 @@ +/******************************************************************************* + * 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.field.converter; + +import quickfix.FieldConvertError; +import quickfix.NumbersCache; + +/** + * Convert between an integer and a String + */ +public final class IntConverter { + + private static final String INT_MAX_STRING = String.valueOf(Integer.MAX_VALUE); + + /** + * Convert an integer to a String + * + * @param i the integer to convert + * @return the String representing the integer + * @see NumbersCache#get(int) + */ + public static String convert(int i) { + return NumbersCache.get(i); + } + + /** + * Convert a String to an integer. + * + * @param value the String to convert + * @return the converted integer + * @throws FieldConvertError raised if the String does not represent a valid + * FIX integer, i.e. optional negative sign and rest are digits. + * @see java.lang.Integer#parseInt(String) + */ + public static int convert(String value) throws FieldConvertError { + + if (!value.isEmpty()) { + final char firstChar = value.charAt(0); + boolean isNegative = (firstChar == '-'); + if (!isDigit(firstChar) && !isNegative) { + throw new FieldConvertError("invalid integral value: " + value); + } + int minLength = (isNegative ? 2 : 1); + if (value.length() < minLength) { + throw new FieldConvertError("invalid integral value: " + value); + } + + // Heuristic: since we have no range check in our parseInt() we only parse + // values which have at least one digit less than Integer.MAX_VALUE and + // leave longer Strings to Integer.parseInt(). + // NB: we must not simply reject strings longer than MAX_VALUE since + // they could possibly include an arbitrary number of leading zeros. + int maxLength = (isNegative ? INT_MAX_STRING.length() : INT_MAX_STRING.length() - 1); + if (value.length() <= maxLength) { + return parseInt(value, isNegative); + } else { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new FieldConvertError("invalid integral value: " + value + ": " + e); + } + } + } else { + throw new FieldConvertError("invalid integral value: empty string"); + } + } + + /** + * Please note that input needs to be validated first, otherwise unexpected + * results may occur. Please also note that this method has no range or + * overflow check, so please only use it when you are sure that no overflow + * might occur (e.g. for parsing seconds or smaller integers). + * + * This method does however check if the contained characters are digits. + * + * @param value the String to convert + * @param isNegative if passed String is negative, first character will + * be skipped since it is assumed that it contains the negative sign + * @return the converted int + */ + private static int parseInt(String value, boolean isNegative) throws FieldConvertError { + int num = 0; + int firstIndex = (isNegative ? 1 : 0); + for (int i = firstIndex; i < value.length(); i++) { + if (isDigit(value.charAt(i))) { + num = (num * 10) + (value.charAt(i) - '0'); + } else { + throw new FieldConvertError("invalid integral value: " + value); + } + } + return isNegative ? -num : num; + } + + /** + * Please note that input needs to be validated first, otherwise unexpected + * results may occur. Please also note that this method has no range or overflow + * check, so please only use it when you are sure that no overflow might occur + * (e.g. for parsing seconds or smaller integers). + * + * @param value the String to convert + * @param off offset position from which String should be parsed + * @param len length to parse + * @return the converted int + */ + static int parseInt(String value, int off, int len) { + int num = 0; + boolean negative = false; + for (int index = 0; index < len; index++) { + final char charAt = value.charAt(off + index); + if (index == 0 && charAt == '-') { + negative = true; + continue; + } + num = (num * 10) + charAt - '0'; + } + return negative ? -num : num; + } + + /** + * Please note that input needs to be validated first, otherwise unexpected + * results may occur. Please also note that this method has no range or overflow + * check, so please only use it when you are sure that no overflow might occur + * (e.g. for parsing seconds or smaller integers). + * + * @param value the String to convert + * @return the converted long + */ + static long parseLong(String value) { + long num = 0; + boolean negative = false; + for (int index = 0; index < value.length(); index++) { + final char charAt = value.charAt(index); + if (index == 0 && charAt == '-') { + negative = true; + continue; + } + num = (num * 10) + (value.charAt(index) - '0'); + } + return negative ? -num : num; + } + + /** + * Check if a character is a digit, i.e. in the range between 0 and 9. + * + * @param character character to check + * @return true if character is a digit between 0 and 9 + */ + static boolean isDigit(char character) { + return (character >= '0' && character <= '9'); + } + +} diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java b/quickfixj-base/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java rename to quickfixj-base/src/main/java/quickfix/field/converter/UtcDateOnlyConverter.java diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java b/quickfixj-base/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java similarity index 100% rename from quickfixj-core/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java rename to quickfixj-base/src/main/java/quickfix/field/converter/UtcTimeOnlyConverter.java diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java b/quickfixj-base/src/main/java/quickfix/field/converter/UtcTimestampConverter.java similarity index 85% rename from quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java rename to quickfixj-base/src/main/java/quickfix/field/converter/UtcTimestampConverter.java index 21c10571bf..08581fab7b 100644 --- a/quickfixj-core/src/main/java/quickfix/field/converter/UtcTimestampConverter.java +++ b/quickfixj-base/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,7 +15,7 @@ * * Contact ask@quickfixengine.org if any conditions of this licensing * are not clear to you. - ***************************************************************************** */ + ******************************************************************************/ package quickfix.field.converter; import quickfix.UtcTimestampPrecision; @@ -24,10 +24,10 @@ import quickfix.SystemTime; import java.text.DateFormat; +import java.time.DateTimeException; 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; @@ -120,7 +120,7 @@ public static Date convert(String value) throws FieldConvertError { 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)); + timeOffset += IntConverter.parseLong(value.substring(18, LENGTH_INCL_MILLIS)); } return new Date(getMillisForDay(value) + timeOffset); } @@ -136,25 +136,32 @@ public static Date convert(String value) throws FieldConvertError { public static LocalDateTime convertToLocalDateTime(String value) throws FieldConvertError { verifyFormat(value); int length = value.length(); + int ns = 0; + if (length >= LENGTH_INCL_NANOS) { + ns = parseInt(value, 18, 9); + } else if (length == LENGTH_INCL_MICROS) { + ns = parseInt(value, 18, 6) * 1000; + } else if (length == LENGTH_INCL_MILLIS) { + ns = parseInt(value, 18, 3) * 1000000; + } + + int yy = parseInt(value, 0, 4); + int mm = parseInt(value, 4, 2); + int dd = parseInt(value, 6, 2); + int h = parseInt(value, 9, 2); + int m = parseInt(value, 12, 2); + int s = parseInt(value, 15, 2); 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) { + return LocalDateTime.of(yy, mm, dd, h, m, s, ns); + } catch (DateTimeException e) { throwFieldConvertError(value, TYPE); } return null; - } + } + + private static int parseInt(String value, int off, int len) { + return IntConverter.parseInt(value, off, len); + } private static Long getMillisForDay(String value) { // Performance optimization: the calendar for the start of the day is cached. @@ -162,9 +169,9 @@ private static Long getMillisForDay(String value) { } private static long getTimeOffsetSeconds(String value) { - return (parseLong(value.substring(9, 11)) * 3600000L) - + (parseLong(value.substring(12, 14)) * 60000L) - + (parseLong(value.substring(15, LENGTH_INCL_SECONDS)) * 1000L); + return (IntConverter.parseLong(value.substring(9, 11)) * 3600000L) + + (IntConverter.parseLong(value.substring(12, 14)) * 60000L) + + (IntConverter.parseLong(value.substring(15, LENGTH_INCL_SECONDS)) * 1000L); } private static void verifyFormat(String value) throws FieldConvertError { diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/package.html b/quickfixj-base/src/main/java/quickfix/field/converter/package.html similarity index 100% rename from quickfixj-core/src/main/java/quickfix/field/converter/package.html rename to quickfixj-base/src/main/java/quickfix/field/converter/package.html diff --git a/quickfixj-base/src/main/xsl/addRequiredFields.xsl b/quickfixj-base/src/main/xsl/addRequiredFields.xsl new file mode 100644 index 0000000000..10ed4ae5f6 --- /dev/null +++ b/quickfixj-base/src/main/xsl/addRequiredFields.xsl @@ -0,0 +1,36 @@ + + + + + + + + + + + Used when a message is sent via a "hub" or "service bureau". If A sends to Q (the hub) who then sends to B via a separate FIX session, then when Q sends to B the value of this field should represent the SendingTime on the message A sent to Q. (always expressed in UTC (Universal Time Coordinated, also known as "GMT") + + + + + + + + + + + + + + + + + + + diff --git a/quickfixj-base/src/main/xsl/extractRequiredFields.xsl b/quickfixj-base/src/main/xsl/extractRequiredFields.xsl new file mode 100644 index 0000000000..be7aa86e48 --- /dev/null +++ b/quickfixj-base/src/main/xsl/extractRequiredFields.xsl @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickfixj-core/src/test/java/quickfix/BusinessRejectReasonTextTest.java b/quickfixj-base/src/test/java/quickfix/BusinessRejectReasonTextTest.java similarity index 100% rename from quickfixj-core/src/test/java/quickfix/BusinessRejectReasonTextTest.java rename to quickfixj-base/src/test/java/quickfix/BusinessRejectReasonTextTest.java diff --git a/quickfixj-base/src/test/java/quickfix/DataDictionaryTest.java b/quickfixj-base/src/test/java/quickfix/DataDictionaryTest.java new file mode 100644 index 0000000000..97c87c1359 --- /dev/null +++ b/quickfixj-base/src/test/java/quickfix/DataDictionaryTest.java @@ -0,0 +1,797 @@ +/******************************************************************************* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.net.URL; +import java.net.URLClassLoader; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import quickfix.field.MsgType; +import quickfix.field.NoHops; + +public class DataDictionaryTest { + + @Rule + public final ExpectedException expectedException = ExpectedException.none(); + + @Test + public void testDictionary() throws Exception { + DataDictionary dd = getDictionary(); + + assertEquals("wrong field name", "Currency", dd.getFieldName(15)); + assertEquals("wrong value description", "BUY", dd.getValueName(4, "B")); + assertEquals("wrong value for given value name", "2", dd.getValue(54, "SELL")); + assertEquals("wrong value type", FieldType.STRING, dd.getFieldType(1)); + assertEquals("wrong version", FixVersions.BEGINSTRING_FIX44, dd.getVersion()); + assertFalse("unexpected field values existence", dd.hasFieldValue(1)); + assertTrue("unexpected field values nonexistence", dd.hasFieldValue(4)); + assertFalse("unexpected field existence", dd.isField(9999)); + assertTrue("unexpected field nonexistence", dd.isField(4)); + assertTrue("unexpected field value existence", !dd.isFieldValue(4, "C")); + assertTrue("unexpected field value nonexistence", dd.isFieldValue(4, "B")); + assertTrue("wrong group info", dd.isGroup("A", 384)); + assertFalse("wrong group info", dd.isGroup("A", 1)); + assertNotNull("wrong group info", dd.getGroup("6", 232)); + assertTrue("incorrect header field", dd.isHeaderField(8)); + assertFalse("incorrect header field", dd.isHeaderField(1)); + assertTrue("incorrect trailer field", dd.isTrailerField(89)); + assertFalse("incorrect trailer field", dd.isTrailerField(1)); + assertTrue("incorrect message field", dd.isMsgField("A", 98)); + assertFalse("incorrect message field", dd.isMsgField("A", 1)); + // component field + assertTrue("incorrect message field", dd.isMsgField("6", 235)); + // group->component field + //assertTrue("incorrect message field", dd.isMsgField("6", 311)); + assertTrue("incorrect message type", dd.isMsgType("A")); + assertFalse("incorrect message type", dd.isMsgType("%")); + assertTrue("incorrect field requirement", dd.isRequiredField("A", 98)); + assertFalse("incorrect field requirement", dd.isRequiredField("A", 95)); + assertEquals("incorrect field name", "Account", dd.getFieldName(1)); + assertEquals("incorrect msg type", "0", dd.getMsgType("Heartbeat")); + assertEquals("incorrect msg type", "B", dd.getMsgType("News")); + assertFalse(dd.isMsgField("UNKNOWN_TYPE", 1)); + } + + @Test + public void testMissingFieldAttributeForRequired() throws Exception { + String data = ""; + data += ""; + data += "
"; + data += " "; + data += "
"; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
"; + + assertConfigErrorForMissingAttributeRequired(data); + } + + private void assertConfigErrorForMissingAttributeRequired(String data) { + try { + new DataDictionary(new ByteArrayInputStream(data.getBytes())); + } catch (ConfigError e) { + // Expected + assertTrue(e.getMessage().contains("does not have a 'required'")); + } + } + + @Test + public void testMissingComponentAttributeForRequired() throws Exception { + String data = ""; + data += ""; + data += "
"; + data += " "; + data += "
"; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
"; + + assertConfigErrorForMissingAttributeRequired(data); + } + + @Test + public void testMissingGroupAttributeForRequired() throws Exception { + String data = ""; + data += ""; + data += "
"; + data += " "; + data += "
"; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
"; + + assertConfigErrorForMissingAttributeRequired(data); + } + + @Test + public void testHeaderTrailerRequired() throws Exception { + String data = ""; + data += ""; + data += "
"; + data += " "; + data += " "; + data += "
"; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
"; + + DataDictionary dd = new DataDictionary(new ByteArrayInputStream(data.getBytes())); + assertEquals(1, dd.getNumMessageCategories()); + assertEquals("0", dd.getMsgType("Heartbeat")); + + assertTrue("BeginString should be required", dd.isRequiredHeaderField(8)); + assertFalse("OnBehalfOfCompID should not be required", dd.isRequiredHeaderField(115)); + assertTrue("Checksum should be required", dd.isRequiredTrailerField(10)); + assertFalse("Signature should not be required", dd.isRequiredTrailerField(89)); + + // now tests for fields that aren't actually in the dictionary - should come back false + assertFalse("Unknown header field shows up as required", dd.isRequiredHeaderField(666)); + assertFalse("Unknown trailer field shows up as required", dd.isRequiredTrailerField(666)); + } + + @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 in 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 in 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 in 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 in 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 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(); + assertTrue(dd.isHeaderGroup(NoHops.FIELD)); + } + + // QF C++ treats the string argument as a filename although it's + // named 'url'. QFJ string argument can be either but this test + // ensures the DD works correctly with a regular file path. + @Test + public void testDictionaryWithFilename() throws Exception { + DataDictionary dd = new DataDictionary("FIX40.xml"); + assertEquals("wrong field name", "Currency", dd.getFieldName(15)); + // It worked! + } + + // Support finding DD in classpath + @Test + public void testDictionaryInClassPath() throws Exception { + URLClassLoader customClassLoader = new URLClassLoader(new URL[] { new URL("file:etc") }, + getClass().getClassLoader()); + Thread currentThread = Thread.currentThread(); + ClassLoader previousContextClassLoader = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(customClassLoader); + try { + DataDictionary dd = new DataDictionary("FIX40.xml"); + assertEquals("wrong field name", "Currency", dd.getFieldName(15)); + // It worked! + } finally { + currentThread.setContextClassLoader(previousContextClassLoader); + } + } + + // QFJ-235 + @Test + public void testWildcardEnumValue() throws Exception { + DataDictionary dd = getDictionary(); + assertTrue(dd.isFieldValue(65, "FOO")); + } + + @Test + public void testMessageCategory() throws Exception { + DataDictionary dd = getDictionary(); + assertTrue(dd.isAdminMessage(MsgType.LOGON)); + assertFalse(dd.isAppMessage(MsgType.LOGON)); + assertFalse(dd.isAdminMessage(MsgType.NEW_ORDER_SINGLE)); + assertTrue(dd.isAppMessage(MsgType.NEW_ORDER_SINGLE)); + } + + // QFJ-535 + @Test + public void testValidateFieldsOutOfOrderForGroups() throws Exception { + final DataDictionary dictionary = new DataDictionary(getDictionary()); + dictionary.setCheckUnorderedGroupFields(false); + Message messageWithGroupLevel1 = new Message( + "8=FIX.4.4\0019=185\00135=D\00134=25\00149=SENDER\00156=TARGET\00152=20110412-13:43:00\001" + + "60=20110412-13:43:00\0011=testAccount\00111=123\00121=3\00138=42\00140=2\00144=42.37\001" + + "54=1\00155=QFJ\00159=0\00178=1\00179=allocAccount\001736=currency\001661=1\00110=130\001", + dictionary); + dictionary.validate(messageWithGroupLevel1); + + Message messageWithGroupLevel2 = new Message( + "8=FIX.4.4\0019=185\00135=D\00134=25\00149=SENDER\00156=TARGET\00152=20110412-13:43:00\001" + + "60=20110412-13:43:00\0011=testAccount\00111=123\00121=3\00138=42\00140=2\00144=42.37\001" + + "54=1\00155=QFJ\00159=0\00178=1\00179=allocAccount\001539=1\001524=1\001538=1\001525=a\00110=145\001", + dictionary); + dictionary.validate(messageWithGroupLevel2); + } + + @Test + public void shouldLoadDictionaryWhenExternalDTDisEnabled() throws ConfigError { + new DataDictionary("FIX_External_DTD.xml", DocumentBuilderFactory::newInstance); + } + + @Test + public void shouldFailToLoadDictionaryWhenExternalDTDisDisabled() { + try { + new DataDictionary("FIX_External_DTD.xml"); + fail("should fail to load dictionary with external DTD"); + } catch (ConfigError e) { + assertEquals("External DTD: Failed to read external DTD 'mathml.dtd', because 'http' access is not allowed due to restriction set by the accessExternalDTD property.", e.getCause().getCause().getMessage()); + } + } + + /** + * For FIX.Latest a minor version is not required. + */ + @Test + public void testMissingMinorVersion() throws Exception { + String data = ""; + data += ""; + data = getCommonDataDictionaryString(data); + + DataDictionary dataDictionary = new DataDictionary(new ByteArrayInputStream(data.getBytes())); + assertEquals(0, dataDictionary.getMinorVersion()); + } + + @Test + public void testFixLatestMajorVersion() throws Exception { + String data = ""; + data += ""; + data = getCommonDataDictionaryString(data); + + DataDictionary dataDictionary = new DataDictionary(new ByteArrayInputStream(data.getBytes())); + assertEquals(0, dataDictionary.getMinorVersion()); + assertEquals("FIX.Latest", dataDictionary.getFullVersion()); + } + + @Test + public void testFixLatestMajorVersionAndEP() throws Exception { + String data = ""; + data += ""; + data = getCommonDataDictionaryString(data); + + DataDictionary dataDictionary = new DataDictionary(new ByteArrayInputStream(data.getBytes())); + assertEquals(0, dataDictionary.getMinorVersion()); + assertEquals("FIX.Latest_EP260", dataDictionary.getFullVersion()); + } + + @Test + public void testSP() throws Exception { + String data = ""; + data += ""; + data = getCommonDataDictionaryString(data); + + DataDictionary dataDictionary = new DataDictionary(new ByteArrayInputStream(data.getBytes())); + assertEquals(0, dataDictionary.getMinorVersion()); + assertEquals("FIX.5.0", dataDictionary.getVersion()); + assertEquals("FIX.5.0SP2", dataDictionary.getFullVersion()); + } + + @Test + public void testEPAndSP() throws Exception { + String data = ""; + data += ""; + data = getCommonDataDictionaryString(data); + + DataDictionary dataDictionary = new DataDictionary(new ByteArrayInputStream(data.getBytes())); + assertEquals(0, dataDictionary.getMinorVersion()); + assertEquals("FIX.5.0", dataDictionary.getVersion()); + assertEquals("FIX.5.0SP2_EP260", dataDictionary.getFullVersion()); + } + + private String getCommonDataDictionaryString(String data) { + data += "
"; + data += " "; + data += "
"; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += " "; + data += "
"; + return data; + } + + + // + // Group Validation Tests in RepeatingGroupTest + // + + private static DataDictionary testDataDictionary; + + /** + * Returns a singleton FIX 4.4 data dictionary. + * NOTE: the returned dictionary must not be modified in any way + * (e.g. by calling any of its setter methods). If it needs to + * be modified, it can be cloned by using the + * {@link DataDictionary#DataDictionary(DataDictionary) + * DataDictionary copy constructor}. + * + * @return a singleton FIX 4.4 data dictionary + * @throws Exception if the data dictionary cannot be loaded + */ + public static DataDictionary getDictionary() throws Exception { + if (testDataDictionary == null) { + testDataDictionary = getDictionary("FIX44.xml"); + } + return testDataDictionary; + } + + /** + * Loads and returns the named data dictionary. + * + * @param fileName the data dictionary file name (e.g. "FIX44.xml") + * @return a new data dictionary instance + * @throws Exception if the named data dictionary cannot be loaded + */ + public static DataDictionary getDictionary(String fileName) throws Exception { + return new DataDictionary(DataDictionaryTest.class.getClassLoader() + .getResourceAsStream(fileName)); + } + +} diff --git a/quickfixj-core/src/test/java/quickfix/DayConverterTest.java b/quickfixj-base/src/test/java/quickfix/DayConverterTest.java similarity index 100% rename from quickfixj-core/src/test/java/quickfix/DayConverterTest.java rename to quickfixj-base/src/test/java/quickfix/DayConverterTest.java diff --git a/quickfixj-core/src/test/java/quickfix/DictionaryTest.java b/quickfixj-base/src/test/java/quickfix/DictionaryTest.java similarity index 100% rename from quickfixj-core/src/test/java/quickfix/DictionaryTest.java rename to quickfixj-base/src/test/java/quickfix/DictionaryTest.java diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/timer/TimerTest.java b/quickfixj-base/src/test/java/quickfix/ExceptionTest.java similarity index 58% rename from quickfixj-core/src/test/java/quickfix/test/acceptance/timer/TimerTest.java rename to quickfixj-base/src/test/java/quickfix/ExceptionTest.java index 5d6600d192..e87eb0adf1 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/timer/TimerTest.java +++ b/quickfixj-base/src/test/java/quickfix/ExceptionTest.java @@ -17,27 +17,30 @@ * are not clear to you. ******************************************************************************/ -package quickfix.test.acceptance.timer; +package quickfix; import junit.framework.TestCase; -import quickfix.ConfigError; -import quickfix.SessionNotFound; -public class TimerTest extends TestCase { +public class ExceptionTest extends TestCase { - TimerTestServer server; + public void testDoNotSend() { + new DoNotSend(); + } - public void testAcceptorTimer() throws ConfigError, SessionNotFound, InterruptedException { - new TimerTestClient().run(); + public void testIncorrectDataFormat() { + IncorrectDataFormat e = new IncorrectDataFormat(5, "test"); + assertEquals(5, e.getField()); + assertEquals("test", e.getData()); } - protected void setUp() throws Exception { - server = new TimerTestServer(); - server.start(); - server.waitForInitialization(); + public void testIncorrectTagValue() { + new IncorrectTagValue(5); + IncorrectTagValue e = new IncorrectTagValue(5, "test"); } - protected void tearDown() throws Exception { - server.stop(); + public void testRuntimeError() { + new RuntimeError(); + new RuntimeError("test"); + new RuntimeError(new Exception()); } } diff --git a/quickfixj-core/src/test/java/quickfix/FieldConvertersTest.java b/quickfixj-base/src/test/java/quickfix/FieldConvertersTest.java similarity index 94% rename from quickfixj-core/src/test/java/quickfix/FieldConvertersTest.java rename to quickfixj-base/src/test/java/quickfix/FieldConvertersTest.java index f12be1c719..ff55918433 100644 --- a/quickfixj-core/src/test/java/quickfix/FieldConvertersTest.java +++ b/quickfixj-base/src/test/java/quickfix/FieldConvertersTest.java @@ -53,6 +53,14 @@ public class FieldConvertersTest { @Test public void testIntegerConversion() throws Exception { + String intMaxValuePlus3 = "2147483650"; + String intMinValueMinus3 = "-2147483651"; + String intLargeValue = "999999999"; + String intLargeValue2 = "2000000000"; + assertEquals(String.valueOf(Integer.MAX_VALUE), IntConverter.convert(Integer.MAX_VALUE)); + assertEquals(String.valueOf(Integer.MIN_VALUE), IntConverter.convert(Integer.MIN_VALUE)); + assertEquals(999999999, IntConverter.convert(intLargeValue)); + assertEquals(2000000000, IntConverter.convert(intLargeValue2)); assertEquals("123", IntConverter.convert(123)); assertEquals(123, IntConverter.convert("123")); assertEquals(-1, IntConverter.convert("-1")); @@ -75,6 +83,38 @@ public void testIntegerConversion() throws Exception { } catch (FieldConvertError e) { // expected } + // this should fail and not overflow + try { + IntConverter.convert(intMaxValuePlus3); + fail(); + } catch (FieldConvertError e) { + // expected + } + // this should fail and not overflow + try { + IntConverter.convert(intMinValueMinus3); + fail(); + } catch (FieldConvertError e) { + // expected + } + try { + IntConverter.convert(""); + fail(); + } catch (FieldConvertError e) { + // expected + } + try { + IntConverter.convert("-"); + fail(); + } catch (FieldConvertError e) { + // expected + } + try { + IntConverter.convert("+"); + fail(); + } catch (FieldConvertError e) { + // expected + } } @Test diff --git a/quickfixj-base/src/test/java/quickfix/FieldMapTest.java b/quickfixj-base/src/test/java/quickfix/FieldMapTest.java new file mode 100644 index 0000000000..30490b9adc --- /dev/null +++ b/quickfixj-base/src/test/java/quickfix/FieldMapTest.java @@ -0,0 +1,83 @@ +package quickfix; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Iterator; +import java.util.Optional; + +import org.junit.Test; + +/** + * Tests the {@link FieldMap} class. + * Specifically, verifies that the setters for {@link UtcTimeStampField} work correctly. + * + * @author toli + */ +public class FieldMapTest { + + private void testOrdering(int[] vals, int[] order, int[] expected) { + FieldMap map = new Message(order); + for (int v : vals) + map.setInt(v, v); + Iterator> it = map.iterator(); + for (int e : expected) + assertEquals(String.valueOf(e), it.next().getObject()); + } + + @Test + public void testOrdering() { + testOrdering(new int[]{1, 2, 3}, null, new int[]{1, 2, 3}); + testOrdering(new int[]{3, 2, 1}, null, new int[]{1, 2, 3}); + testOrdering(new int[]{1, 2, 3}, new int[]{1, 2, 3}, new int[]{1, 2, 3}); + testOrdering(new int[]{3, 2, 1}, new int[]{1, 2, 3}, new int[]{1, 2, 3}); + testOrdering(new int[]{1, 2, 3}, new int[]{1, 3, 2}, new int[]{1, 3, 2}); + testOrdering(new int[]{3, 2, 1}, new int[]{1, 3, 2}, new int[]{1, 3, 2}); + testOrdering(new int[]{1, 2, 3}, new int[]{1, 3}, new int[]{1, 3, 2}); + testOrdering(new int[]{3, 2, 1}, new int[]{1, 3}, new int[]{1, 3, 2}); + testOrdering(new int[]{1, 2, 3}, new int[]{3, 1}, new int[]{3, 1, 2}); + testOrdering(new int[]{3, 2, 1}, new int[]{3, 1}, new int[]{3, 1, 2}); + } + + @Test + 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()); + } + + @Test + public void testOptionalDecimal() { + FieldMap map = new Message(); + map.setField(new DecimalField(44, new BigDecimal("1565.10"))); + Optional optValue = map.getOptionalDecimal(44); + assertTrue(optValue.isPresent()); + assertEquals(0, optValue.get().compareTo(new BigDecimal("1565.10"))); + assertFalse(map.getOptionalDecimal(6).isPresent()); + } + + @Test + public void testNullFieldException() { + FieldMap map = new Message(); + StringField field = new StringField(0, null); + assertThrows(FieldException.class, () -> map.setField(field)); + } + + @Test + public void testRemoveGroup() { + FieldMap map = new Message(); + Group group = new Group(73, 11); + map.addGroup(group); + assertTrue(map.hasGroup(73)); + map.removeGroup(73); + assertFalse(map.hasGroup(73)); + } +} diff --git a/quickfixj-base/src/test/java/quickfix/FieldTest.java b/quickfixj-base/src/test/java/quickfix/FieldTest.java new file mode 100644 index 0000000000..a42f7c560d --- /dev/null +++ b/quickfixj-base/src/test/java/quickfix/FieldTest.java @@ -0,0 +1,234 @@ +/******************************************************************************* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Date; + +import org.junit.Test; +import org.quickfixj.CharsetSupport; + +public class FieldTest { + + private void testFieldCalcuations(String value, int checksum, int length) { + Field field = new Field<>(12, value); + field.setObject(value); + assertEquals("12=" + value, field.toString()); + assertEquals(checksum, field.getChecksum()); + assertEquals(length, field.getLength()); + + value = value.substring(0, value.length() - 1) + (char)(value.charAt(value.length() - 1) + 1); + checksum = (checksum + 1) & 0xFF; + field.setObject(value); + assertEquals("12=" + value, field.toString()); + assertEquals(checksum, field.getChecksum()); + assertEquals(length, field.getLength()); + + field.setTag(13); + checksum = (checksum + 1) & 0xFF; + assertEquals("13=" + value, field.toString()); + assertEquals(checksum, field.getChecksum()); + assertEquals(length, field.getLength()); + } + + @Test + public void testFieldCalculationsWithDefaultCharset() { + testFieldCalcuations("VALUE", 30, 9); + } + + @Test + public void testFieldCalculationsWithUTF8Charset() throws UnsupportedEncodingException { + CharsetSupport.setCharset("UTF-8"); + try { + testFieldCalcuations("\u6D4B\u9A8C\u6570\u636E", 50, 16); + } finally { + CharsetSupport.setCharset(CharsetSupport.getDefaultCharset()); + } + } + + @Test + public void testDateField() { + DateField field = new DateField(11); + Date date = new Date(); + field.setValue(date); + assertEquals(11, field.getTag()); + assertEquals(date, field.getValue()); + field = new DateField(11, date); + assertEquals(11, field.getTag()); + assertEquals(date, field.getValue()); + } + + @Test + public void testUtcDateOnlyField() { + UtcDateOnlyField field = new UtcDateOnlyField(11); + LocalDate date = LocalDate.now(); + field.setValue(date); + assertEquals(11, field.getTag()); + assertEquals(date, field.getValue()); + field = new UtcDateOnlyField(11, date); + assertEquals(11, field.getTag()); + assertEquals(date, field.getValue()); + } + + @Test + public void testUtcTimeOnlyField() { + UtcTimeOnlyField field = new UtcTimeOnlyField(11); + LocalTime date = LocalTime.now(); + field.setValue(date); + assertEquals(11, field.getTag()); + assertEquals(date, field.getValue()); + field = new UtcTimeOnlyField(11, date); + assertEquals(11, field.getTag()); + assertEquals(date, field.getValue()); + } + + @Test + public void testUtcTimeStampField() { + UtcTimeStampField field = new UtcTimeStampField(11); + LocalDateTime date = LocalDateTime.now(); + field.setValue(date); + assertEquals(11, field.getTag()); + assertEquals(date, field.getValue()); + field = new UtcTimeStampField(11, date); + assertEquals(11, field.getTag()); + assertEquals(date, field.getValue()); + } + + @Test + public void testBooleanField() { + BooleanField field = new BooleanField(11); + field.setValue(true); + assertEquals(11, field.getTag()); + assertTrue(field.getValue()); + field.setValue(Boolean.FALSE); + assertEquals(11, field.getTag()); + assertFalse(field.getValue()); + field = new BooleanField(22, true); + assertEquals(22, field.getTag()); + assertTrue(field.getValue()); + field = new BooleanField(33, Boolean.TRUE); + assertEquals(33, field.getTag()); + assertTrue(field.getValue()); + } + + @Test + public void testDoubleField() { + DoubleField field = new DoubleField(11); + field.setValue(12.3); + assertEquals(11, field.getTag()); + assertEquals(12.3, field.getValue(), 0); + field.setValue(new Double(23.4)); + assertEquals(11, field.getTag()); + assertEquals(23.4, field.getValue(), 0); + field = new DoubleField(22, 34.5); + assertEquals(22, field.getTag()); + assertEquals(34.5, field.getValue(), 0); + field = new DoubleField(33, new Double(45.6)); + assertEquals(33, field.getTag()); + assertEquals(45.6, field.getValue(), 0); + } + + @Test(expected = NumberFormatException.class) + public void testDoubleFieldException() { + DoubleField field = new DoubleField(11, Double.NaN); + } + + @Test + public void testDecimalField() { + DecimalField field = new DecimalField(11); + field.setValue(12.3); + assertEquals(11, field.getTag()); + assertEquals(BigDecimal.valueOf(12.3), field.getValue()); + field.setValue(23.4); + assertEquals(11, field.getTag()); + assertEquals(BigDecimal.valueOf(23.4), field.getValue()); + field = new DecimalField(22, 34.5); + assertEquals(22, field.getTag()); + assertEquals(BigDecimal.valueOf(34.5), field.getValue()); + field = new DecimalField(33, 45.6); + assertEquals(33, field.getTag()); + assertEquals(BigDecimal.valueOf(45.6), field.getValue()); + } + + @Test(expected = NumberFormatException.class) + public void testDecimalFieldException() { + DecimalField field = new DecimalField(11, Double.POSITIVE_INFINITY); + } + + @Test + public void testCharField() { + CharField field = new CharField(11); + field.setValue('x'); + assertEquals(11, field.getTag()); + assertEquals('x', field.getValue()); + field.setValue(Character.valueOf('X')); + assertEquals(11, field.getTag()); + assertEquals('X', field.getValue()); + field = new CharField(22, 'a'); + assertEquals(22, field.getTag()); + assertEquals('a', field.getValue()); + field = new CharField(33, Character.valueOf('A')); + assertEquals(33, field.getTag()); + assertEquals('A', field.getValue()); + } + + @Test + public void testIntField() { + IntField field = new IntField(11); + field.setValue(12); + assertEquals(11, field.getTag()); + assertEquals(12, field.getValue()); + field.setValue(Integer.valueOf(23)); + assertEquals(11, field.getTag()); + assertEquals(23, field.getValue()); + field = new IntField(22, 23); + assertEquals(22, field.getTag()); + assertEquals(23, field.getValue()); + field = new IntField(33, Integer.valueOf(44)); + assertEquals(33, field.getTag()); + assertEquals(44, field.getValue()); + } + + @Test + public void testFieldhashCode() throws Exception { + assertEqualsAndHash(new IntField(11, 100), new IntField(11, 100)); + assertEqualsAndHash(new DoubleField(11, 100.0), new DoubleField(11, 100.0)); + 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')); + 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)); + } + + private void assertEqualsAndHash(Field field1, Field field2) { + assertEquals("fields not equal", field1, field2); + assertEquals("fields hashcode not equal", field1.hashCode(), field2.hashCode()); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/FileUtilTest.java b/quickfixj-base/src/test/java/quickfix/FileUtilTest.java similarity index 90% rename from quickfixj-core/src/test/java/quickfix/FileUtilTest.java rename to quickfixj-base/src/test/java/quickfix/FileUtilTest.java index 16cffdc225..56b10e29ce 100644 --- a/quickfixj-core/src/test/java/quickfix/FileUtilTest.java +++ b/quickfixj-base/src/test/java/quickfix/FileUtilTest.java @@ -43,7 +43,7 @@ public void testFileLocation() throws Exception { @Test public void testClassResourceLocation() throws Exception { - InputStream in = FileUtil.open(Message.class, "Session.class"); + InputStream in = FileUtil.open(Message.class, "FixVersions.class"); in.close(); assertNotNull("Resource not found", in); } @@ -67,6 +67,15 @@ public void testURLLocation() throws Exception { } } + @Test + public void testJARURLLocation() throws Exception { + // just test that we don't run into a ClassCastException + InputStream in = FileUtil.open(Message.class, "jar:file:/foo.bar!/"); + if (in != null) { + in.close(); + } + } + @Test // QFJ-775 public void testSessionIDFileName() { diff --git a/quickfixj-base/src/test/java/quickfix/MessageTest.java b/quickfixj-base/src/test/java/quickfix/MessageTest.java new file mode 100644 index 0000000000..91432d48a5 --- /dev/null +++ b/quickfixj-base/src/test/java/quickfix/MessageTest.java @@ -0,0 +1,741 @@ +package quickfix; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Calendar; +import java.util.TimeZone; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import quickfix.field.ApplExtID; +import quickfix.field.ApplVerID; +import quickfix.field.BeginString; +import quickfix.field.BodyLength; +import quickfix.field.CheckSum; +import quickfix.field.CstmApplVerID; +import quickfix.field.MsgSeqNum; +import quickfix.field.MsgType; +import quickfix.field.SecureData; +import quickfix.field.SenderCompID; +import quickfix.field.SendingTime; +import quickfix.field.SessionRejectReason; +import quickfix.field.Signature; +import quickfix.field.SignatureLength; +import quickfix.field.TargetCompID; +import quickfix.field.TargetSubID; + +public class MessageTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + + @Test + public void testRepeatingField() throws Exception { + final Message m = new Message( + "8=FIX.4.0\0019=100\00135=D\00134=2\00149=TW\00156=ISLD\00111=ID\00121=1\001" + + "40=1\00154=1\00140=2\00138=200\00155=INTC\00110=160\001"); + assertFalse("message should be invalid", m.hasValidStructure()); + assertEquals("wrong invalid tag", 40, m.getInvalidTag()); + } + + @Test + public void testHeaderCustomFieldOrdering() throws Exception { + + class MyMessage extends Message { + + final int[] headerFieldOrder = { + BeginString.FIELD, + BodyLength.FIELD, + MsgType.FIELD, + TargetSubID.FIELD, + SendingTime.FIELD, + MsgSeqNum.FIELD, + SenderCompID.FIELD, + TargetCompID.FIELD + }; + + public MyMessage() { + super(); + header = new Header(headerFieldOrder); + } + } + + final MyMessage myMessage = new MyMessage(); + + myMessage.getHeader().setField(new SenderCompID("foo")); + myMessage.getHeader().setField(new MsgSeqNum(22)); + myMessage.getHeader().setString(SendingTime.FIELD, "20120922-11:00:00"); + myMessage.getHeader().setField(new TargetCompID("bar")); + + assertTrue(myMessage.toString().contains("52=20120922-11:00:00\00134=22\00149=foo\00156=bar")); + } + + @Test + public void testHeaderFieldWithCustomTransportDictionaryConstructorReadsHeaderField() throws Exception { + + final DataDictionary customSessionDictionary = new DataDictionary("FIXT11_Custom_Test.xml"); + customSessionDictionary.setAllowUnknownMessageFields(false); + + final DataDictionary standardSessionDictionary = new DataDictionary("FIXT11.xml"); + standardSessionDictionary.setAllowUnknownMessageFields(false); + + final DataDictionary applicationDictionary = new DataDictionary("FIX50.xml"); + applicationDictionary.setAllowUnknownMessageFields(false); + + final String sep = "\001"; + final StringBuilder sb = new StringBuilder(); + sb.append("8=FIXT1.1"); + sb.append(sep); + sb.append("9=112"); + sb.append(sep); + sb.append("35=6"); + sb.append(sep); + sb.append("49=SENDER_COMP_ID"); + sb.append(sep); + sb.append("56=TARGET_COMP_ID"); + sb.append(sep); + sb.append("34=20"); + sb.append(sep); + sb.append("52=20120922-11:00:00"); + sb.append(sep); + sb.append("12312=foo"); + sb.append(sep); + sb.append("23=123456"); + sb.append(sep); + sb.append("28=N"); + sb.append(sep); + sb.append("55=[N/A]"); + sb.append(sep); + sb.append("54=1"); + sb.append(sep); + sb.append("27=U"); + sb.append(sep); + sb.append("10=52"); + sb.append(sep); + final String messageData = sb.toString(); + + final Message standardMessage = new Message(messageData, standardSessionDictionary, applicationDictionary, true); + + // Test that field is in body not the header + assertTrue(standardMessage.toString().contains("12312=foo")); + assertFalse(standardMessage.getHeader().isSetField(12312)); + assertTrue(standardMessage.isSetField(12312)); + assertEquals("foo", standardMessage.getString(12312)); + + // Test that field is correctly classified in header with customSessionDictionary + final Message customMessage = new Message(messageData, customSessionDictionary, applicationDictionary, true); + assertTrue(customMessage.toString().contains("12312=foo")); + assertTrue(customMessage.getHeader().isSetField(12312)); + assertEquals("foo", customMessage.getHeader().getString(12312)); + assertFalse(customMessage.isSetField(12312)); + } + + @Test + public void testTrailerCustomFieldOrdering() throws Exception { + + class MyMessage extends Message { + + final int[] trailerFieldOrder = {Signature.FIELD, SignatureLength.FIELD, CheckSum.FIELD}; + + public MyMessage() { + super(); + trailer = new Trailer(trailerFieldOrder); + } + } + + final MyMessage myMessage = new MyMessage(); + + myMessage.getTrailer().setField(new Signature("FOO")); + myMessage.getTrailer().setField(new SignatureLength(3)); + assertTrue(myMessage.toString().contains("89=FOO\00193=3\001")); + } + + @Test + public void testFix5HeaderFields() { + assertTrue(Message.isHeaderField(ApplVerID.FIELD)); + assertTrue(Message.isHeaderField(CstmApplVerID.FIELD)); + } + + @Test + public void testApplExtIDIsHeaderField() { + assertTrue(Message.isHeaderField(ApplExtID.FIELD)); + } + + @Test + public void testHeaderFieldsMissing() throws Exception { + try { + new Message("1=FIX.4.2"); + } catch (final InvalidMessage e) { + // expected + } + } + + @Test + public void testMessageFromString() { + Message message = null; + + boolean badMessage = false; + try { + message = new Message("8=FIX.4.2\0019=12\00135=A\001108=30\00110=036\001"); + } catch (final InvalidMessage e) { + badMessage = true; + } + assertTrue("Message should be invalid", badMessage); + + try { + message = new Message("8=FIX.4.2\0019=12\00135=A\001108=30\00110=026\001"); + } catch (final InvalidMessage e) { + fail("Message should be valid (" + e.getMessage() + ")"); + } + assertEquals("8=FIX.4.2\0019=12\00135=A\001108=30\00110=026\001", message.toString()); + } + + @Test + public void testIsEmpty() { + final Message message = new Message(); + assertTrue("Message should be empty on construction", message.isEmpty()); + message.getHeader().setField(new BeginString("FIX.4.2")); + assertFalse("Header should contain a field", message.isEmpty()); + message.clear(); + assertTrue("Message should be empty after clear", message.isEmpty()); + message.setField(0, new Field(20000, "MSFT")); + assertFalse("Body should contain a field", message.isEmpty()); + message.clear(); + assertTrue("Message should be empty after clear", message.isEmpty()); + message.getTrailer().setField(new CheckSum("10")); + assertFalse("Trailer should contain a field", message.isEmpty()); + message.clear(); + assertTrue("Message should be empty after clear", message.isEmpty()); + } + + @Test + public void testMessageSetGetString() { + final Message message = new Message(); + + try { + message.getString(5); + fail("exception not thrown"); + } catch (final FieldNotFound e) { + } + + message.setString(5, "string5"); + + try { + assertEquals("string5", message.getString(5)); + } catch (final FieldNotFound e) { + fail("exception thrown"); + } + + expectedException.expect(FieldException.class); + message.setString(100, null); + } + + @Test + public void testMessageSetGetBoolean() { + final Message message = new Message(); + + try { + message.getBoolean(7); + fail("exception not thrown"); + } catch (final FieldNotFound e) { + } + + message.setBoolean(7, true); + + try { + assertTrue(message.getBoolean(7)); + } catch (final FieldNotFound e) { + fail("exception thrown"); + } + } + + @Test + public void testMessageSetGetChar() { + final Message message = new Message(); + + try { + message.getChar(12); + fail("exception not thrown"); + } catch (final FieldNotFound e) { + } + + message.setChar(12, 'a'); + + try { + assertEquals('a', message.getChar(12)); + } catch (final FieldNotFound e) { + fail("exception thrown"); + } + } + + @Test + public void testMessageSetGetChars() throws FieldNotFound { + final Message message = new Message(); + + try { + message.getChars(18); + fail("exception not thrown"); + } catch (final FieldNotFound e) { + } + + message.setChars(18, 'a', 'b', '4'); + assertArrayEquals(new char[]{'a', 'b', '4'}, message.getChars(18)); + } + + @Test + public void testMessageSetGetCharsInvalidFormatException() throws FieldNotFound { + expectedException.expect(FieldException.class); + expectedException.expectMessage("invalid char array: [65, 32, 98, 32, 48, 53]"); + + final Message message = new Message(); + message.setString(123, "A b 05"); + message.getChars(123); + } + + @Test + public void testMessageSetGetInt() { + final Message message = new Message(); + + try { + message.getInt(56); + fail("exception not thrown"); + } catch (final FieldNotFound e) { + } + + message.setInt(56, 23); + + try { + assertEquals(23, message.getInt(56)); + } catch (final FieldNotFound e) { + fail("exception thrown"); + } + } + + @Test + public void testMessageSetGetDouble() { + final Message message = new Message(); + + try { + message.getDouble(9812); + fail("exception not thrown"); + } catch (final FieldNotFound e) { + } + + message.setDouble(9812, 12.3443); + + try { + assertEquals(12.3443, message.getDouble(9812), 1e-10); + } catch (final FieldNotFound e) { + fail("exception thrown"); + } + } + + @Test + public void testMessageSetGetUtcTimeStamp() { + final Message message = new Message(); + + try { + message.getUtcTimeStamp(8); + fail("exception not thrown"); + } catch (final FieldNotFound e) { + } + + final TimeZone timezone = TimeZone.getTimeZone("GMT+0"); + final Calendar calendar = Calendar.getInstance(timezone); + calendar.set(2002, 8, 6, 12, 34, 56); + calendar.set(Calendar.MILLISECOND, 0); + + final LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(calendar.getTimeInMillis()), ZoneOffset.UTC); + message.setUtcTimeStamp(8, time); + + try { + assertEquals(message.getUtcTimeStamp(8), time); + } catch (final FieldNotFound e) { + fail("exception thrown"); + } + } + + @Test + public void testRemoveField() { + final Message message = new Message(); + message.setField(new StringField(12, "value")); + assertTrue(message.isSetField(12)); + message.removeField(12); + assertTrue(!message.isSetField(12)); + } + + @Test + public void testMessageIterator() { + Message message = new Message(); + java.util.Iterator> i = message.iterator(); + assertFalse(i.hasNext()); + try { + assertNull(i.next()); + fail("exception not thrown"); + } catch (final java.util.NoSuchElementException e) { + } + + try { + message = new Message("8=FIX.4.2\0019=12\00135=A\001108=30\00110=026\001"); + i = message.iterator(); + assertTrue(i.hasNext()); + StringField field = (StringField) i.next(); + assertEquals(108, field.getField()); + assertEquals("30", field.getValue()); + + assertFalse(i.hasNext()); + try { + assertNull(i.next()); + fail("exception not thrown"); + } catch (final java.util.NoSuchElementException e) { + } + + final java.util.Iterator> j = message.getHeader().iterator(); + assertTrue(j.hasNext()); + field = (StringField) j.next(); + assertEquals(8, field.getField()); + assertEquals("FIX.4.2", field.getValue()); + field = (StringField) j.next(); + assertEquals(9, field.getField()); + assertEquals("12", field.getValue()); + field = (StringField) j.next(); + assertEquals(35, field.getField()); + assertEquals("A", field.getValue()); + + assertFalse(j.hasNext()); + try { + assertNull(j.next()); + fail("exception not thrown"); + } catch (final java.util.NoSuchElementException e) { + } + } catch (final InvalidMessage e) { + fail("exception thrown"); + } + } + + @Test + public void testIsAdmin() { + final Message message = new Message(); + + message.getHeader().setString(MsgType.FIELD, MsgType.HEARTBEAT); + assertTrue(message.isAdmin()); + + message.getHeader().setString(MsgType.FIELD, MsgType.LOGON); + assertTrue(message.isAdmin()); + + message.getHeader().setString(MsgType.FIELD, MsgType.LOGOUT); + assertTrue(message.isAdmin()); + + message.getHeader().setString(MsgType.FIELD, MsgType.SEQUENCE_RESET); + assertTrue(message.isAdmin()); + + message.getHeader().setString(MsgType.FIELD, MsgType.RESEND_REQUEST); + assertTrue(message.isAdmin()); + + message.getHeader().setString(MsgType.FIELD, MsgType.TEST_REQUEST); + assertTrue(message.isAdmin()); + + message.getHeader().setString(MsgType.FIELD, MsgType.REJECT); + assertTrue(message.isAdmin()); + + message.getHeader().setString(MsgType.FIELD, MsgType.NEW_ORDER_SINGLE); + assertFalse(message.isAdmin()); + + message.getHeader().setString(MsgType.FIELD, MsgType.QUOTE_RESPONSE); + assertFalse(message.isAdmin()); + } + + + /** + * Verify that an empty message can still be "printed" and doesn't result in any exceptions + */ + @Test + public void testEmptyMessageToString() throws Exception { + final Message msg = new quickfix.Message(); + assertNotNull(msg.toString()); + assertTrue("empty message contains no checksum", msg.toString().length() > 0); + } + + @Test + public void testParseEmptyString() throws Exception { + final String data = ""; + + // with validation + try { + new Message(data, DataDictionaryTest.getDictionary()); + } catch (final InvalidMessage im) { + } catch (final Throwable e) { + e.printStackTrace(); + fail("InvalidMessage expected, got " + e.getClass().getName()); + } + + // without validation + try { + new Message(data, DataDictionaryTest.getDictionary(), false); + } catch (final InvalidMessage im) { + } catch (final Throwable e) { + e.printStackTrace(); + fail("InvalidMessage expected, got " + e.getClass().getName()); + } + } + + /** + * Test for data fields with SOH. This test is based on report from a user on + * the QuickFIX mailing list. The problem was the user's configuration but this + * seems like a good unit test to keep in the suite. + */ + @Test + public void testDataFieldParsing() throws Exception { + final String data = "10001=Canonical.1.00\00110002=001058\00125001=01\00110003=SAPI_ADMRESP\00110004=SUBSCRIBE_RESP\001" + + "10009=705\00110012=01\00110005=SPGW\00110006=SAPI\00110007=0\00110010=16:25:11.537\001" + + "10045=SDQADL:01:/SDB/ENT/@/@/STKSDLL:7\00110955=Y\00110963=043\00110961=03\00111285=N\001" + + "11339=823,980\00110919=N\00111111=86795696\00110898=043\00110920=~\00110938=N\00111340=5- 9.99\001" + + "11343=0.20\00111344=~\00111341=~\00111342=0.15\00111345=10- 14.99\00111348=0.25\00111349=~\00111346=~\001" + + "11347=0.15\00111350=15- 19.99\00111353=0.30\00111354=~\00111351=~\00111352=0.20\00111338=23SEP05\001" + + "10981=0\00110485=N\00110761=0\00111220=~\00111224=N\00110808=N\00110921=~\00110960=N\00110957=N\00111329=N\001" + + "11286=0\00111214=USA\00110917=Y\00111288=0\00110906=N\00110737=0.01\00110956=~\00110967=~\00110965=~\00110809=0\001" + + "10762=N\00110763=N\00110712=1\00110905=09:30:00\00110918=YA0101\00110951=Y\00110469=1\00110949=1\00110487=Q\00110950=Y\001" + + "10899=N\00110380=N\00110696=03\00111082=18.41\00110217=12\00110954=N\00110708=E\00110958=N\00111213=US \00111334=N\001" + + "11332=N\00111331=N\00111330=N\00111335=N\00111333=N\00110767=3\00110974=~\00110980=AIRTRAN HOLDINGS \00111289=N\001" + + "10912=4\00110915=0501\00110914=0501\00110975=N\00110913=SLK\00110698=055\00110666=AAI\00110903=S\00111328=N\001" + + "10624=L\00111287=0\00110699=0\00110962=L\00111227=SUB1\00111229=5\00111228=1\00111236=16:24:41.521\00111277=16:25:11.630\001"; + + try { + final DataDictionary dictionary = DataDictionaryTest.getDictionary(); + final Message m = new Message(("8=FIX.4.4\0019=1144\00135=A\001" + + "98=0\001384=2\001372=D\001385=R\001372=8\001385=S\00195=1092\001" + "96=" + + data + "\00110=5\001"), dictionary); + assertEquals(1144, m.bodyLength()); + final Message m2 = new Message(m.toString(), dictionary); + assertEquals(1144, m2.bodyLength()); + } catch (final InvalidMessage e) { + fail(e.getMessage()); + } + } + + @Test + public void testHeaderFieldInBody() throws Exception { + final Message message = new Message("8=FIX.4.2\0019=40\00135=A\001" + + "98=0\001212=4\001384=2\001372=D\001385=R\001372=8\001385=S\00110=103\001", + DataDictionaryTest.getDictionary()); + + assertFalse(message.hasValidStructure()); + + assertTrue(message.getHeader().isSetField(212)); + + assertEquals(SessionRejectReason.TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER, message + .getException().getSessionRejectReason()); + assertEquals(212, message.getException().getField()); + } + + @Test + public void testTrailerFieldInBody() throws Exception { + final Message message = new Message("8=FIX.4.2\0019=40\00135=A\001" + + "98=0\00193=5\001384=2\001372=D\001385=R\001372=8\001385=S\00110=63\001", + DataDictionaryTest.getDictionary()); + + assertFalse(message.hasValidStructure()); + + final SignatureLength signatureLength = new SignatureLength(); + message.getTrailer().getField(signatureLength); + assertEquals(5, signatureLength.getValue()); + } + + // Includes test for QFJ-413. Repeating group check for size = 0 + @Test + public void testMessageGroupCountValidation() throws Exception { + final String data = "8=FIX.4.4\0019=222\00135=D\00149=SenderCompId\00156=TargetCompId\00134=37\001" + + "52=20070223-22:28:33\00111=183339\00122=8\00138=1\00140=2\00144=12\00148=BHP\00154=2\001" + + "55=BHP\00159=1\00160=20060223-22:38:33\001526=3620\00178=0\00179=AllocACC1\00180=1010.1\001" + + "79=AllocACC2\00180=2020.2\001453=2\001448=8\001447=D\001452=4\001448=AAA35354\001447=D\001452=3\00110=079\001"; + final Message message = new Message(); + final DataDictionary dd = DataDictionaryTest.getDictionary(); + message.fromString(data, dd, true); + try { + dd.validate(message); + fail("No exception thrown"); + } catch (final FieldException e) { + final String emsg = e.getMessage(); + assertNotNull("No exception message", emsg); + assertTrue(emsg.startsWith("Incorrect NumInGroup")); + } + } + + /** + * QFJ-760 + */ + @Test + public void testMessageWithMissingChecksumField() throws Exception { + // checksum is "merged" into field 452, i.e. SOH is missing between field 452 and 10 + String badMessage = "8=FIX.4.4\0019=275\00135=D\00134=3\00149=441000-XXXXX-X-XXXX-001\001" + + "52=20131113-10:22:31.567\00156=XXXXX\0011=A1\00111=9fef3663330e209e1bce\00118=H\001" + + "22=4\00138=200\00140=M\00148=XX0005519XXXX\00154=1\00155=[N/A]\00158=MassTest\00159=0\001" + + "60=20131113-10:22:31.567\001100=XXXX\001526=9fef3663330e209e1bce\001453=1\001" + + "448=XXXXXXXX030\001447=D\001452=3610=016\001"; + + Message msg = new Message(); + try { + msg.fromString(badMessage, DataDictionaryTest.getDictionary(), true); + fail(); + } catch (final InvalidMessage e) { + final String emsg = e.getMessage(); + assertNotNull("No exception message", emsg); + assertTrue(emsg.startsWith("Field not found")); + } + } + + @Test + public void testFalseMessageStructureException() { + try { + final DataDictionary dd = DataDictionaryTest.getDictionary(); + // duplicated tag 98 + // QFJ-65 + new Message("8=FIX.4.4\0019=22\00135=A\00198=0\00198=0\001108=30\00110=223\001", dd, + true); + // For now, this will not cause an exception if the length and checksum are correct + } catch (final Exception e) { + final String text = e.getMessage(); + assertTrue("Wrong exception message: " + text, !text.contains("Actual body length")); + } + } + + @Test + public void testComponentInGroup() { + try { + final DataDictionary dd = DataDictionaryTest.getDictionary(); + // duplicated tag 98 + // QFJ-65 + // 8=FIX.4.4\0019=941\00135=AE\00149=ICE\00134=63\00152=20091117-18:59:04.780\00156=XXXX\001 + // 57=X\001571=219449\001487=0\001856=0\001828=0\001150=F\00117=44750544433\00139=2\001 + // 570=N\00155=480120\00148=WBS FMG0010-BRN FMG0010\00122=8\001461=FXXXXX\001916=20100201\001 + // 917=20100228\00132=1.0\00131=0.69\0019018=1\0019022=1\00175=20091117\00160=20091117-18:59:04.775\001 + // 552=1\00154=2\00137=41296064\00111=557859232\001453=7\001448=trader\001447=D\001452=11\001 + // 448=Trading Corp\001447=D\001452=13\001448=2757\001447=D\001452=56\001448=805\001447=D\001 + // 452=4\001448=11122556\001447=D\001452=51\001448=FCM\001447=D\001452=60\001448=U\001447=D\001 + // 452=5 4\00158=41293051\001555=2\001600=460130\001602=WBS FMG0010!\001603=8\001608=FXXXXX\001 + // 624=2\001637=80.78\001687=1.0\001654=41296074\0019019=1\0019023=1\0019020=20100201\001 + // 9021=20100228\001539=4\001524=805\001525=D\001538=4\001524=11122556\001525=D\001538=51\001 + // 524=FCM\001525=D\001538=60 524=U\001525=D\001538=54\001600=217927\001 + // 602=BRN FMG0010! 63=8 608-FXXXXX 624=1 637=80.09 687=1.0 654=41296073 9019=1 9023=1 9020=20100201 9021=20100228 539=4 524=805\001 + // 525=D\001538=4\001524=11122556 525=D\001538=51 524=Newedge 525=D 538=60 524=U 525=D 538=54 10=112 + new Message( + "8=FIX.4.4\0019=941\00135=AE\00149=ICE\00134=63\00152=20091117-18:59:04.780\00156=XXXX\001" + + "57=X\001571=219449\001487=0\001856=0\001828=0\001150=F\00117=44750544433\00139=2\001" + + "570=N\00155=480120\00148=WBS FMG0010-BRN FMG0010\00122=8\001461=FXXXXX\001916=20100201\001" + + "917=20100228\00132=1.0\00131=0.69\0019018=1\0019022=1\00175=20091117\00160=20091117-18:59:04.775\001" + + "552=1\00154=2\00137=41296064\00111=557859232\001453=7\001448=trader\001447=D\001452=11\001" + + "448=Trading Corp\001447=D\001452=13\001448=2757\001447=D\001452=56\001448=805\001447=D\001" + + "452=4\001448=11122556\001447=D\001452=51\001448=FCM\001447=D\001452=60\001448=U\001447=D\001" + + "452=5 4\00158=41293051\001555=2\001600=460130\001602=WBS FMG0010!\001603=8\001608=FXXXXX\001" + + "624=2\001637=80.78\001687=1.0\001654=41296074\0019019=1\0019023=1\0019020=20100201\001" + + "9021=20100228\001539=4\001524=805\001525=D\001538=4\001524=11122556\001525=D\001538=51\001" + + "524=FCM\001525=D\001538=60 524=U\001525=D\001538=54\001600=217927\001602=BRN FMG0010!\001" + + "63=8 608-FXXXXX\001624=1\001637=80.09\001687=1.0\001654=41296073\0019019=1\0019023=1\001" + + "9020=20100201\001021=20100228\001539=4\001524=805\001525=D\001538=4\001524=11122556\001" + + "525=D\001538=51\001524=FCM\001525=D\001538=60 524=U\001525=D\001538=54\001600=217927\001" + + "602=BRN FMG0010!\00163=8 608-FXXXXX\001624=1\001637=80.09\001687=1.0\001654=41296073\001" + + "9019=1\0019023=1\0019020=20100201\001021=20100228\001", + dd, true); + // For now, this will not cause an exception if the length and checksum are correct + } catch (final Exception e) { + final String text = e.getMessage(); + assertTrue("Wrong exception message: " + text, !text.contains("Actual body length")); + } + } + + @Test + public void testFalseMessageStructureException2() { + try { + final DataDictionary dd = DataDictionaryTest.getDictionary(); + // duplicated raw data length + // QFJ-121 + new Message("8=FIX.4.4\0019=22\00135=A\00196=X\001108=30\00110=223\001", dd, true); + } catch (final Exception e) { + final String text = e.getMessage(); + assertTrue("Wrong exception message: " + text, + text != null && !text.contains("Actual body length")); + } + } + + // 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", "\\|")); + } + + // QFJ-722 + @Test + public void testIfMessageHeaderIsCreatedWithEveryConstructor() throws Exception { + final String rawMessage = "8=FIX.4.2\0019=12\00135=A\001108=30\00110=026\001"; + final DataDictionary dataDictionary = new DataDictionary(DataDictionaryTest.getDictionary()); + + final Message emptyConstructor = new Message(); + assertNotNull(emptyConstructor.getHeader()); + + final Message secondConstructor = new Message(new int[]{}); + assertNotNull(secondConstructor.getHeader()); + + final Message thirdConstructor = new Message(rawMessage); + assertNotNull(thirdConstructor.getHeader()); + + final Message fourthConstructor = new Message(rawMessage, false); + assertNotNull(fourthConstructor.getHeader()); + + final Message fifthConstructor = new Message(rawMessage, dataDictionary); + assertNotNull(fifthConstructor.getHeader()); + + final Message sixthConstructor = new Message(rawMessage, dataDictionary, false); + assertNotNull(sixthConstructor.getHeader()); + + final Message seventhConstructor = new Message(rawMessage, dataDictionary, dataDictionary, false); + assertNotNull(seventhConstructor.getHeader()); + } + + // QFJ-66 Should not throw exception when parsing data field in header + @Test + public void testHeaderDataField() throws Exception { + final Message m = new Message("8=FIX.4.2\0019=53\00135=A\00190=4\00191=ABCD\001" + + "98=0\001384=2\001372=D\001385=R\001372=8\001385=S\00110=241\001", + DataDictionaryTest.getDictionary()); + assertEquals("ABCD", m.getHeader().getString(SecureData.FIELD)); + } +} diff --git a/quickfixj-base/src/test/java/quickfix/MessageUtilsTest.java b/quickfixj-base/src/test/java/quickfix/MessageUtilsTest.java new file mode 100644 index 0000000000..fdde176b1a --- /dev/null +++ b/quickfixj-base/src/test/java/quickfix/MessageUtilsTest.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * 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 quickfix.field.BeginString; +import quickfix.field.MsgType; +import quickfix.field.SenderCompID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import org.junit.Test; + +/** + * NOTE: There are two MessageUtilsTests. + * One in quickfixj-base, one in quickfixj-core, which each test + * some functionality. This test excludes some test cases that cannot + * be tested in this module due to classes that are generated in a + * later step, e.g. MessageFactories. + */ +public class MessageUtilsTest { + + @Test + public void testGetStringField() throws Exception { + String messageString = "8=FIX.4.2\0019=12\00135=X\001108=30\00110=049\001"; + assertEquals("wrong value", "FIX.4.2", MessageUtils.getStringField(messageString, + BeginString.FIELD)); + assertEquals("wrong value", "X", MessageUtils.getStringField(messageString, MsgType.FIELD)); + assertNull(messageString, MessageUtils.getStringField(messageString, SenderCompID.FIELD)); + } + + @Test + public void testSessionIdFromRawMessage() throws Exception { + String messageString = "8=FIX.4.0\0019=56\00135=A\00134=1\00149=TW\001" + + "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; + SessionID sessionID = MessageUtils.getSessionID(messageString); + assertEquals(sessionID.getBeginString(), "FIX.4.0"); + assertEquals("TW", sessionID.getSenderCompID()); + assertEquals("ISLD", sessionID.getTargetCompID()); + } + + @Test + public void testReverseSessionIdFromRawMessage() throws Exception { + String messageString = "8=FIX.4.0\0019=56\00135=A\00134=1\00149=TW\00150=TWS\001" + + "142=TWL\00152=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; + SessionID sessionID = MessageUtils.getReverseSessionID(messageString); + assertEquals(sessionID.getBeginString(), "FIX.4.0"); + assertEquals("ISLD", sessionID.getSenderCompID()); + assertEquals("TW", sessionID.getTargetCompID()); + assertEquals("TWS", sessionID.getTargetSubID()); + assertEquals("TWL", sessionID.getTargetLocationID()); + } + + @Test + public void testMessageType() throws Exception { + String messageString = "8=FIX.4.0\0019=56\00135=A\00134=1\00149=TW\001" + + "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; + assertEquals("A", MessageUtils.getMessageType(messageString)); + } + + @Test + public void testMessageTypeError() throws Exception { + String messageString = "8=FIX.4.0\0019=56\00134=1\00149=TW\001" + + "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; + try { + MessageUtils.getMessageType(messageString); + fail("expected exception"); + } catch (InvalidMessage e) { + // expected + } + } + + @Test + public void testMessageTypeError2() throws Exception { + String messageString = "8=FIX.4.0\0019=56\00135=1"; + try { + MessageUtils.getMessageType(messageString); + fail("expected exception"); + } catch (InvalidMessage e) { + // expected + } + } + + @Test + public void testGetNonexistentStringField() throws Exception { + String messageString = "8=FIX.4.0\0019=56\00134=1\00149=TW\001" + + "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; + assertNull(MessageUtils.getStringField(messageString, 35)); + } + + @Test + public void testGetStringFieldWithBadValue() throws Exception { + String messageString = "8=FIX.4.0\0019=56\00134=1\00149=TW\001" + + "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223"; + assertNull(MessageUtils.getStringField(messageString, 10)); + } + +} diff --git a/quickfixj-base/src/test/java/quickfix/MockSystemTimeSource.java b/quickfixj-base/src/test/java/quickfix/MockSystemTimeSource.java new file mode 100644 index 0000000000..3c9dea46c4 --- /dev/null +++ b/quickfixj-base/src/test/java/quickfix/MockSystemTimeSource.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * 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.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Calendar; + +public class MockSystemTimeSource implements SystemTimeSource { + private long[] systemTimes = { System.currentTimeMillis() }; + private int offset; + + public MockSystemTimeSource() { + // empty + } + + public MockSystemTimeSource(long time) { + setSystemTimes(time); + } + + public void setSystemTimes(long[] times) { + systemTimes = times; + } + + void setSystemTimes(long time) { + systemTimes = new long[] { time }; + } + + public void setTime(Calendar c) { + setSystemTimes(c.getTimeInMillis()); + } + + @Override + public long getTime() { + if (systemTimes.length - offset > 1) { + offset++; + } + return systemTimes[offset]; + } + + public void increment(long delta) { + if (systemTimes.length - offset == 1) { + 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/SessionIDTest.java b/quickfixj-base/src/test/java/quickfix/SessionIDTest.java similarity index 100% rename from quickfixj-core/src/test/java/quickfix/SessionIDTest.java rename to quickfixj-base/src/test/java/quickfix/SessionIDTest.java diff --git a/quickfixj-base/src/test/java/quickfix/UTCDateOnlyFieldTest.java b/quickfixj-base/src/test/java/quickfix/UTCDateOnlyFieldTest.java new file mode 100644 index 0000000000..66ac2a6f2e --- /dev/null +++ b/quickfixj-base/src/test/java/quickfix/UTCDateOnlyFieldTest.java @@ -0,0 +1,38 @@ +package quickfix; + +import org.junit.Test; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +import static org.junit.Assert.*; + +public class UTCDateOnlyFieldTest { + + public static final int MD_ENTRY_DATE_FIELD = 272; + + @Test + public void testUtcDateOnlyFieldFromLocalDate() { + LocalDate localDate = LocalDate.of(2023, 3, 19); + UtcDateOnlyField utcDateOnlyField = new UtcDateOnlyField(MD_ENTRY_DATE_FIELD,localDate); + assertTrue(utcDateOnlyField.valueEquals(localDate)); + assertEquals(localDate,utcDateOnlyField.getValue()); + assertEquals(MD_ENTRY_DATE_FIELD, utcDateOnlyField.getField()); + } + + @Test + public void testUtcDateOnlyFieldFromNow() { + UtcDateOnlyField utcDateOnlyField = new UtcDateOnlyField(MD_ENTRY_DATE_FIELD); + assertNotNull(utcDateOnlyField.getValue()); + assertEquals(MD_ENTRY_DATE_FIELD, utcDateOnlyField.getField()); + } + + @Test + public void testAssignment() { + LocalDate localDate = LocalDate.of(2023, 12, 31); + UtcDateOnlyField utcDateOnlyField = new UtcDateOnlyField(MD_ENTRY_DATE_FIELD); + utcDateOnlyField.setValue(localDate); + assertTrue(utcDateOnlyField.valueEquals(localDate)); + assertEquals(localDate,utcDateOnlyField.getValue()); + } +} diff --git a/quickfixj-core/src/test/resources/FIXT11_Custom_Test.xml b/quickfixj-base/src/test/resources/FIXT11_Custom_Test.xml similarity index 100% rename from quickfixj-core/src/test/resources/FIXT11_Custom_Test.xml rename to quickfixj-base/src/test/resources/FIXT11_Custom_Test.xml diff --git a/quickfixj-base/src/test/resources/FIX_External_DTD.xml b/quickfixj-base/src/test/resources/FIX_External_DTD.xml new file mode 100644 index 0000000000..b1953366fd --- /dev/null +++ b/quickfixj-base/src/test/resources/FIX_External_DTD.xml @@ -0,0 +1,27 @@ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + +
diff --git a/quickfixj-base/src/test/resources/configWithSessionVariables.ini b/quickfixj-base/src/test/resources/configWithSessionVariables.ini new file mode 100644 index 0000000000..e06f510dbc --- /dev/null +++ b/quickfixj-base/src/test/resources/configWithSessionVariables.ini @@ -0,0 +1,20 @@ +#comment +[DEFAULT] +Empty= +ConnectionType=acceptor +SocketAcceptPort=5001 +FileStorePath=store +StartTime=00:00:00 +EndTime=00:00:00 +TestLong=1234 +TestLong2=abcd +TestDouble=12.34 +TestDouble2=abcd +TestBoolTrue=Y +TestBoolFalse=N +SenderCompID=TW + +[SESSION] +BeginString=FIX.4.2 +TargetCompID=CLIENT3_${CLIENT_PLACEHOLDER1}_${CLIENT_PLACEHOLDER2} +DataDictionary=../spec/FIX42.xml diff --git a/quickfixj-core/src/test/resources/quickfix/test/acceptance/definitions/client/Normal.def b/quickfixj-base/src/test/resources/quickfix/test/acceptance/definitions/client/Normal.def similarity index 100% rename from quickfixj-core/src/test/resources/quickfix/test/acceptance/definitions/client/Normal.def rename to quickfixj-base/src/test/resources/quickfix/test/acceptance/definitions/client/Normal.def diff --git a/quickfixj-class-pruner-maven-plugin/pom.xml b/quickfixj-class-pruner-maven-plugin/pom.xml new file mode 100644 index 0000000000..812839fc0c --- /dev/null +++ b/quickfixj-class-pruner-maven-plugin/pom.xml @@ -0,0 +1,101 @@ + + + + 4.0.0 + + + org.quickfixj + quickfixj-parent + 3.0.0-SNAPSHOT + + + quickfixj-class-pruner-maven-plugin + maven-plugin + + QuickFIX/J Class Pruner Maven Plugin + + + ${maven.version} + + + + UTF-8 + + + + + + org.apache.maven + maven-plugin-api + provided + + + org.apache.maven + maven-core + provided + + + org.apache.maven + maven-artifact + provided + + + org.apache.maven + maven-compat + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + + + org.apache.maven.shared + file-management + + + org.junit.vintage + junit-vintage-engine + test + + + org.apache.maven.plugin-testing + maven-plugin-testing-harness + test + + + org.codehaus.plexus + plexus-xml + test + + + + + + + maven-surefire-plugin + + + org.apache.maven.plugins + maven-plugin-plugin + + true + + + + mojo-descriptor + + descriptor + + + + help-goal + + helpmojo + + + + + + + diff --git a/quickfixj-class-pruner-maven-plugin/readme.md b/quickfixj-class-pruner-maven-plugin/readme.md new file mode 100644 index 0000000000..1f865f9ce6 --- /dev/null +++ b/quickfixj-class-pruner-maven-plugin/readme.md @@ -0,0 +1,59 @@ +# Class Pruner Maven Plugin + +This plugin has a very specialised purpose. + +The purpose is to minimise the number of Java classes and generated sources for packaging artefacts of specific FIX versions. This is needed because the introduction of FIX Latest standard results in a very large number of Fields that in turn requires excessive memory and build time for the creation of Javadoc artefacts. + +This is a result of the design of QuickFIX/J in that the Fields package is shared in common for multiple versions of the FIX Protocol. Accordingly the Component and Messages packages are compiled against the common package of Fields. The Messages, Components and Fields can be assembled together as in ```quickfixj-messages-all``` or separately as in ```quickfixj-messages-fix40``` to ```quickfixj-messages-fixlatest``` and ```quickfixj-messages-fixt11```. In each of these assemblies the requisite Fields are included. These packages can be used together at runtime without conflict if the Field classes are identical. + +Broadly speaking the later versions of FIX are super-sets of the prior versions. There are cases of Fields being deprecated but in QuickFIX/J distributions these Fields are included in subsequent distributions. + +As mentioned the FIX Latest distribution is very large and it is built in its entirety by QuickFIX/J to verify compatibility. The complete set of Fields is not required for older versions of the standard which are still in common use. + +This plugin can be used before packaging a version of the standard to prune the Java sources and classes that are not included in the FIX specification for that version. This allows Javadoc to be created and produces a more compact and concise distribution. + +The plugin parses the specified QuickFIX dictionaries to identify the required Fields and deletes redundant Java sources and classes from the specified generated sources and classes directories. + +It can be used as follows. + +``` +... + + + + org.quickfixj + class-pruner-maven-plugin + ${project.version} + + + prune + + prune + + + ${project.basedir}/../quickfixj-messages-all/target/classes/quickfix/field + ${project.basedir}/../quickfixj-messages-all/target/generated-sources/quickfix/field + + ${project.basedir}/../quickfixj-messages-all/target/classes/ + + **/*.xml + + + **/FIXLatest.xml + OrchestraFIXLatest.xml + FIX50SP2.xml + FIX50SP1.xml + FIX44.xml + + false + + + + + +... +``` +Note: +* only *later* versions of the FIX specification than the one being packaged are excluded from parsing +* FIX dictionaries not used in the code generation are excluded (redundant Dictionary files are present in the project) +* the use of this plugin depends on correct ordering of the modules with the most recent versions of the FIX protocol being packaged before packaging the older versions of the protocol. diff --git a/quickfixj-class-pruner-maven-plugin/src/main/java/org/quickfixj/ClassPrunerMojo.java b/quickfixj-class-pruner-maven-plugin/src/main/java/org/quickfixj/ClassPrunerMojo.java new file mode 100644 index 0000000000..874d960ff9 --- /dev/null +++ b/quickfixj-class-pruner-maven-plugin/src/main/java/org/quickfixj/ClassPrunerMojo.java @@ -0,0 +1,205 @@ +package org.quickfixj; + + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.shared.model.fileset.FileSet; +import org.apache.maven.shared.model.fileset.util.FileSetManager; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * Goal that prunes specified classes + * This is intended to be used before javadoc and packaging to reduce memory requirements and build time + */ +@Mojo( name = "prune", defaultPhase = LifecyclePhase.INSTALL ) +public class ClassPrunerMojo extends AbstractMojo { + + /** + * fileSet defining the QFJ Dictionary Locations. + */ + @Parameter ( property = "fileset", required = true ) + private FileSet fileset; + + /** + * Location of classes to delete + */ + @Parameter( defaultValue = "classes", property = "classesDirectory", required = true ) + private File classesDirectory; + + /** + * Location of the generated source to delete + */ + @Parameter( defaultValue = "generated-sources", property = "generatedSourcesDirectory", required = true ) + private File generatedSourcesDirectory; + + + public void execute() + throws MojoExecutionException + { + this.getLog().info("executing mojo."); + + if ( !classesDirectory.exists() || !classesDirectory.isDirectory() ) + { + String errorMsg = new StringBuilder(classesDirectory.getAbsolutePath()).append(" must exist and be a directory.").toString(); + this.getLog().error(errorMsg.toString()); + throw new MojoExecutionException( errorMsg.toString() ); + } else { + this.getLog().info(new StringBuilder("Classes Directory : ").append(classesDirectory.getAbsolutePath()).toString()); + } + + if ( !generatedSourcesDirectory.exists() || !generatedSourcesDirectory.isDirectory() ) + { + String errorMsg = new StringBuilder(generatedSourcesDirectory.getAbsolutePath()).append(" must exist and be a directory.").toString(); + this.getLog().error(errorMsg.toString()); + throw new MojoExecutionException( errorMsg.toString() ); + } else { + this.getLog().info(new StringBuilder("Generated Sources Directory : ").append(generatedSourcesDirectory.getAbsolutePath()).toString()); + } + + if (null == fileset) { + String errorMsg = "filset must not be null."; + this.getLog().error(errorMsg); + throw new MojoExecutionException( errorMsg ); + } + + Set fieldNames = new HashSet(); + + collectFieldNames(fieldNames); + + try { + pruneGeneratedSources(fieldNames); + pruneClasses(fieldNames); + } catch (IOException e) { + String errorMsg = "Exception pruning directories."; + this.getLog().error(errorMsg, e); + throw new MojoExecutionException( errorMsg, e); + } + } + + private void pruneClasses(Set fieldNames) throws IOException { + prune(fieldNames, this.classesDirectory, ".class", "Java class"); + } + + private void pruneGeneratedSources(Set fieldNames) throws IOException { + prune(fieldNames, this.generatedSourcesDirectory, ".java", "Java source"); + } + + private void prune(Set fieldNames, File targetDirectory, String fileSuffix, String descriptor) throws IOException { + Set files = listFiles(targetDirectory); + Set namesOfFilesToKeep = fieldNames.stream().map(file -> new StringBuilder(file).append(fileSuffix).toString()).collect(Collectors.toSet()); + files.removeAll(namesOfFilesToKeep); + List fileList = new ArrayList(files); + Collections.sort(fileList); + this.getLog().info(descriptor + "s to delete : " + fileList.size()); + for (String fileName : fileList) { + this.getLog().info("Deleting " + descriptor + " : " + fileName); + File file = new File( targetDirectory, fileName ); + Files.delete(file.toPath()); + } + } + + private static Set listFiles(File directory) throws IOException { + try (Stream stream = Files.list(directory.toPath())) { + return stream + .filter(file -> !Files.isDirectory(file)) + .map(Path::getFileName) + .map(Path::toString) + .filter(fileName -> !fileName.matches(".*\\.xml")) + .collect(Collectors.toSet()); + } + } + + private void collectFieldNames(Set fieldNames) throws MojoExecutionException { + Set fileNamesToParse = collectFileNameToParse(); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db; + try { + db = dbf.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + String msg = "ParserConfigurationException creating Document Builder"; + this.getLog().error(msg, e); + throw new MojoExecutionException(msg, e); + } + + for (String fileName: fileNamesToParse) { + Document document; + try { + document = db.parse(new File(fileName)); + // optional, but recommended + // http://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-java-how-does-it-work + document.getDocumentElement().normalize(); + addNames(document.getDocumentElement(), "fields/field", fieldNames); + } catch (SAXException | IOException e) { + String msg = "Exception parsing file " + fileName; + this.getLog().error(msg, e); + throw new MojoExecutionException(msg, e); + } + } + + List fieldList = new ArrayList(fieldNames); + Collections.sort(fieldList); + for (String fieldName : fieldList) { + this.getLog().info("Found field : " + fieldName); + } + this.getLog().info("Found field total : " + fieldList.size()); + } + + private Set collectFileNameToParse() { + FileSetManager fileSetManager = new FileSetManager(); + this.getLog().info("fileset " + fileset.toString()); + + String currentDir = System.getProperty("user.dir"); + this.getLog().info("Current working directory : " + currentDir); + + Set includedFiles = new HashSet(Arrays.asList(fileSetManager.getIncludedFiles( fileset ))); + Set fileNamesToParse = new HashSet(); + + String baseDirectory = fileset.getDirectory(); + for (String includedFile: includedFiles) { + this.getLog().info("will parse file : " + includedFile); + String fileName = new StringBuilder(baseDirectory).append(File.separator).append(includedFile).toString(); + fileNamesToParse.add(fileName); + } + return fileNamesToParse; + } + + private static void addNames(Element element, String path, Set fieldNames) { + int separatorOffset = path.indexOf("/"); + if (separatorOffset == -1) { + NodeList fieldNodeList = element.getElementsByTagName(path); + for (int i = 0; i < fieldNodeList.getLength(); i++) { + fieldNames.add(((Element) fieldNodeList.item(i)).getAttribute("name")); + } + } else { + String tag = path.substring(0, separatorOffset); + NodeList subnodes = element.getElementsByTagName(tag); + for (int i = 0; i < subnodes.getLength(); i++) { + addNames((Element) subnodes.item(i), path.substring(separatorOffset + 1), fieldNames); + } + } + } +} diff --git a/quickfixj-class-pruner-maven-plugin/src/test/java/org/quickfixj/ClassPrunerMojoTest.java b/quickfixj-class-pruner-maven-plugin/src/test/java/org/quickfixj/ClassPrunerMojoTest.java new file mode 100644 index 0000000000..cf94698f3c --- /dev/null +++ b/quickfixj-class-pruner-maven-plugin/src/test/java/org/quickfixj/ClassPrunerMojoTest.java @@ -0,0 +1,408 @@ +package org.quickfixj; + + +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.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.commons.io.FileUtils; +import org.apache.maven.plugin.testing.MojoRule; +import org.apache.maven.shared.model.fileset.FileSet; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; + +import com.google.common.io.Files; + +public class ClassPrunerMojoTest +{ + private List classesList = new ArrayList(Arrays.asList( + "Account" + ,"AccruedInterestAmt" + ,"AccruedInterestRate" + ,"AdvId" + ,"AdvRefID" + ,"AdvSide" + ,"AdvTransType" + ,"AllocAccount" + ,"AllocAvgPx" + ,"AllocHandlInst" + ,"AllocID" + ,"AllocLinkID" + ,"AllocLinkType" + ,"AllocNetMoney" + ,"AllocRejCode" + ,"AllocShares" + ,"AllocStatus" + ,"AllocText" + ,"AllocTransType" + ,"AvgPrxPrecision" + ,"AvgPx" + ,"BeginSeqNo" + ,"BeginString" + ,"BidForwardPoints" + ,"BidPx" + ,"BidSize" + ,"BidSpotRate" + ,"BodyLength" + ,"BrokerOfCredit" + ,"CashOrderQty" + ,"CashSettlAgentAcctName" + ,"CashSettlAgentAcctNum" + ,"CashSettlAgentCode" + ,"CashSettlAgentContactName" + ,"CashSettlAgentContactPhone" + ,"CashSettlAgentName" + ,"CheckSum" + ,"ClientID" + ,"ClOrdID" + ,"Commission" + ,"CommType" + ,"CoveredOrUncovered" + ,"CumQty" + ,"Currency" + ,"CustomerOrFirm" + ,"CxlQty" + ,"CxlRejReason" + ,"CxlType" + ,"DeliverToCompID" + ,"DeliverToLocationID" + ,"DeliverToSubID" + ,"DKReason" + ,"DlvyInst" + ,"EffectiveTime" + ,"EmailThreadID" + ,"EmailType" + ,"EncryptMethod" + ,"EndSeqNo" + ,"ExDestination" + ,"ExecBroker" + ,"ExecID" + ,"ExecInst" + ,"ExecRefID" + ,"ExecTransType" + ,"ExecType" + ,"ExpireTime" + ,"ForexReq" + ,"FutSettDate" + ,"FutSettDate2" + ,"GapFillFlag" + ,"HandlInst" + ,"Headline" + ,"HeartBtInt" + ,"IDSource" + ,"IOIID" + ,"IOINaturalFlag" + ,"IOIOthSvc" + ,"IOIQltyInd" + ,"IOIQualifier" + ,"IOIRefID" + ,"IOIShares" + ,"IOITransType" + ,"Issuer" + ,"LastCapacity" + ,"LastForwardPoints" + ,"LastMkt" + ,"LastPx" + ,"LastShares" + ,"LastSpotRate" + ,"LeavesQty" + ,"LinesOfText" + ,"ListExecInst" + ,"ListID" + ,"ListNoOrds" + ,"ListSeqNo" + ,"LocateReqd" + ,"MaturityDay" + ,"MaturityMonthYear" + ,"MaxFloor" + ,"MaxShow" + ,"MinQty" + ,"MiscFeeAmt" + ,"MiscFeeCurr" + ,"MiscFeeType" + ,"MsgSeqNum" + ,"MsgType" + ,"NetMoney" + ,"NewSeqNo" + ,"NoAllocs" + ,"NoDlvyInst" + ,"NoExecs" + ,"NoIOIQualifiers" + ,"NoMiscFees" + ,"NoOrders" + ,"NoRelatedSym" + ,"NoRpts" + ,"NotifyBrokerOfCredit" + ,"NumDaysInterest" + ,"OfferForwardPoints" + ,"OfferPx" + ,"OfferSize" + ,"OfferSpotRate" + ,"OnBehalfOfCompID" + ,"OnBehalfOfLocationID" + ,"OnBehalfOfSubID" + ,"OpenClose" + ,"OptAttribute" + ,"OrderID" + ,"OrderQty" + ,"OrderQty2" + ,"OrdRejReason" + ,"OrdStatus" + ,"OrdType" + ,"OrigClOrdID" + ,"OrigSendingTime" + ,"OrigTime" + ,"PegDifference" + ,"PossDupFlag" + ,"PossResend" + ,"PrevClosePx" + ,"Price" + ,"ProcessCode" + ,"PutOrCall" + ,"QuoteID" + ,"QuoteReqID" + ,"RawData" + ,"RawDataLength" + ,"RefAllocID" + ,"RefSeqNum" + ,"RelatdSym" + ,"ReportToExch" + ,"ResetSeqNumFlag" + ,"RptSeq" + ,"Rule80A" + ,"SecondaryOrderID" + ,"SecureData" + ,"SecureDataLen" + ,"SecurityDesc" + ,"SecurityExchange" + ,"SecurityID" + ,"SecuritySettlAgentAcctName" + ,"SecuritySettlAgentAcctNum" + ,"SecuritySettlAgentCode" + ,"SecuritySettlAgentContactName" + ,"SecuritySettlAgentContactPhone" + ,"SecuritySettlAgentName" + ,"SecurityType" + ,"SenderCompID" + ,"SenderLocationID" + ,"SenderSubID" + ,"SendingTime" + ,"SettlBrkrCode" + ,"SettlCurrAmt" + ,"SettlCurrency" + ,"SettlCurrFxRate" + ,"SettlCurrFxRateCalc" + ,"SettlDeliveryType" + ,"SettlDepositoryCode" + ,"SettlInstCode" + ,"SettlInstID" + ,"SettlInstMode" + ,"SettlInstSource" + ,"SettlInstTransType" + ,"SettlLocation" + ,"SettlmntTyp" + ,"Shares" + ,"Side" + ,"Signature" + ,"SignatureLength" + ,"StandInstDbID" + ,"StandInstDbName" + ,"StandInstDbType" + ,"StopPx" + ,"StrikePrice" + ,"Subject" + ,"Symbol" + ,"SymbolSfx" + ,"TargetCompID" + ,"TargetLocationID" + ,"TargetSubID" + ,"TestReqID" + ,"Text" + ,"TimeInForce" + ,"TradeDate" + ,"TransactTime" + ,"Urgency" + ,"URLLink" + ,"ValidUntilTime" + ,"ValuationBusinessCenter" + ,"ValuationDate" + ,"ValuationMethod" + ,"ValuationReferenceModel" + ,"ValuationSource" + ,"ValuationTime" + ,"ValueCheckAction" + ,"ValueCheckType" + ,"ValueOfFutures" + ,"VegaMultiplier" + ,"VenueType" + ,"VerificationMethod" + ,"VersusPurchaseDate" + ,"VersusPurchasePrice" + ,"Volatility" + ,"VoluntaryRegulatoryReport" + ,"WarningText" + ,"WaveNo" + ,"WireReference" + ,"WorkingIndicator" + ,"WtAverageLiquidity" + ,"Yield" + ,"YieldCalcDate" + ,"YieldRedemptionDate" + ,"YieldRedemptionPrice" + ,"YieldRedemptionPriceType" + ,"YieldType")); + + List someFieldNamesThatShouldBePruned = + new ArrayList<>(Arrays.asList("ValuationBusinessCenter", + "ValuationDate", + "ValuationMethod", + "ValuationReferenceModel", + "ValuationSource", + "ValuationTime", + "ValueCheckAction", + "ValueCheckType", + "ValueOfFutures", + "VegaMultiplier", + "VenueType", + "VerificationMethod", + "VersusPurchaseDate", + "VersusPurchasePrice", + "Volatility", + "VoluntaryRegulatoryReport", + "WarningText", + "WireReference", + "WorkingIndicator", + "WtAverageLiquidity", + "Yield", + "YieldCalcDate", + "YieldRedemptionDate", + "YieldRedemptionPrice", + "YieldRedemptionPriceType", + "YieldType")); + private File classesDirectory; + private File generatedSourcesDirectory; + + @Rule + public MojoRule rule = new MojoRule() + { + @Override + protected void before() throws Throwable + { + } + + @Override + protected void after() + { + } + }; + + @After + public void clearDown() throws Exception { + ClassPrunerMojoTest.clearDownDirectory(generatedSourcesDirectory); + ClassPrunerMojoTest.clearDownDirectory(classesDirectory); + } + + private static void clearDownDirectory(File directory) throws Exception { + if (null != directory && directory.exists()) { + FileUtils.cleanDirectory(directory); + } + directory.delete(); + } + + /** + * This test represents pruning Field classes that do not exist if FIX 5.0sp2 and earlier. + * The inputs are QFJ dictionaries for FIX 5.0sp2 (and some earlier versions) and + * directories that contain files with the names of Fields found in FIX Latest. + * The files for Fields found in FIX 5.0sp2 and earlier are retained, the excess files are pruned. + * @throws Exception if any + */ + @Test + public void testPrune() + throws Exception + { + File pom = new File("target/test-classes/project-to-test/"); + assertNotNull( pom ); + assertTrue( pom.exists() ); + + ClassPrunerMojo myMojo = ( ClassPrunerMojo ) rule.lookupConfiguredMojo( pom, "prune" ); + assertNotNull( myMojo ); + + classesDirectory = ( File ) rule.getVariableValueFromObject( myMojo, "classesDirectory" ); + assertNotNull( classesDirectory ); + if (!classesDirectory.exists()) { + classesDirectory.mkdirs(); + } + //TODO create directory, populate with test files + myMojo.getLog().info("classesDirectory : " + classesDirectory.getAbsolutePath() ); + createFilesForTest(classesList, classesDirectory, ".class"); + createFilesForTest(someFieldNamesThatShouldBePruned, classesDirectory, ".class"); + + generatedSourcesDirectory = ( File ) rule.getVariableValueFromObject( myMojo, "generatedSourcesDirectory" ); + assertNotNull( generatedSourcesDirectory ); + if (!generatedSourcesDirectory.exists()) { + generatedSourcesDirectory.mkdirs(); + } + myMojo.getLog().info("generatedSourcesDirectory : " + generatedSourcesDirectory.getAbsolutePath() ); + createFilesForTest(classesList, generatedSourcesDirectory, ".java"); + createFilesForTest(someFieldNamesThatShouldBePruned, generatedSourcesDirectory, ".java"); + + myMojo.execute(); + + Object variableValueFromObject = rule.getVariableValueFromObject( myMojo, "fileset" ); + FileSet dictionaryFileSet = ( FileSet ) variableValueFromObject; + assertNotNull( dictionaryFileSet ); + + // The following are based on files created above + List someFieldNamesThatShouldStillExist = new ArrayList<>(Arrays.asList("WaveNo", "ValidUntilTime", "Account")); + + for (String fieldName : someFieldNamesThatShouldStillExist) { + File source = new File( generatedSourcesDirectory, fieldName.concat(".java") ); + assertTrue( source.exists() ); + File clazz = new File( classesDirectory, fieldName.concat(".class") ); + assertTrue( clazz.exists() ); + } + + for (String fieldName : someFieldNamesThatShouldBePruned) { + File source = new File( generatedSourcesDirectory, fieldName.concat(".java") ); + assertFalse( source.exists() ); + File clazz = new File( classesDirectory, fieldName.concat(".class") ); + assertFalse( clazz.exists() ); + } + + int numberOfFieldsFromTheCombinedDictionaries = 209; + assertEquals(classesDirectory.list().length, numberOfFieldsFromTheCombinedDictionaries); + assertEquals(generatedSourcesDirectory.list().length, numberOfFieldsFromTheCombinedDictionaries); + } + + private static void createFilesForTest(List classesList, File classesDirectory, String extension) throws IOException { + classesList.stream().forEach(throwingConsumerWrapper(f -> {File file = new File(classesDirectory, f.concat(extension)); Files.touch(file);})); + } + + @FunctionalInterface + public interface ThrowingConsumer { + void accept(T t) throws E; + } + + //https://www.baeldung.com/java-lambda-exceptions + static Consumer throwingConsumerWrapper( + ThrowingConsumer throwingConsumer) { + return i -> { + try { + throwingConsumer.accept(i); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + +} + diff --git a/quickfixj-class-pruner-maven-plugin/src/test/resources/project-to-test/pom.xml b/quickfixj-class-pruner-maven-plugin/src/test/resources/project-to-test/pom.xml new file mode 100644 index 0000000000..96513ba048 --- /dev/null +++ b/quickfixj-class-pruner-maven-plugin/src/test/resources/project-to-test/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + org.quickfixj + project-to-test + 1.0-SNAPSHOT + jar + Test MyMojo + + + + + org.quickfixj + quickfixj-class-pruner-maven-plugin + + not-really-classes + not-really-generated-sources + + target/test-classes/qfj-dictionaries + + **/*.xml + + + **/FIX50SP2.modified.xml + **/FIXT11.xml + + false + + + + + + diff --git a/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIX40.xml b/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIX40.xml new file mode 100644 index 0000000000..514a89d65f --- /dev/null +++ b/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIX40.xml @@ -0,0 +1,857 @@ + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIX41.xml b/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIX41.xml new file mode 100644 index 0000000000..458497c537 --- /dev/null +++ b/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIX41.xml @@ -0,0 +1,1268 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIX50SP2.modified.xml b/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIX50SP2.modified.xml new file mode 100644 index 0000000000..069cf4837b --- /dev/null +++ b/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIX50SP2.modified.xml @@ -0,0 +1,9942 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIXT11.xml b/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIXT11.xml new file mode 100644 index 0000000000..d763b29641 --- /dev/null +++ b/quickfixj-class-pruner-maven-plugin/src/test/resources/qfj-dictionaries/FIXT11.xml @@ -0,0 +1,383 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/quickfixj-codegenerator/pom.xml b/quickfixj-codegenerator/pom.xml index 01dec3e7d3..0bb96d8ca8 100644 --- a/quickfixj-codegenerator/pom.xml +++ b/quickfixj-codegenerator/pom.xml @@ -4,7 +4,7 @@ org.quickfixj quickfixj-parent - 2.2.0-SNAPSHOT + 3.0.0-SNAPSHOT quickfixj-codegenerator @@ -25,17 +25,32 @@ org.apache.maven maven-plugin-api - 3.6.3 + provided org.apache.maven maven-project - 2.2.1 + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + provided net.sf.saxon Saxon-HE - 9.8.0-4 + 12.4 + + + org.junit.vintage + junit-vintage-engine + test + + + commons-io + commons-io + test @@ -61,6 +76,10 @@ maven-pmd-plugin + + maven-plugin-plugin + 3.10.2 + diff --git a/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/CodeGenerationException.java b/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/CodeGenerationException.java index e0cb060d2a..ec208887cb 100644 --- a/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/CodeGenerationException.java +++ b/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/CodeGenerationException.java @@ -23,6 +23,11 @@ * Signals an error in the code generation software. */ public class CodeGenerationException extends RuntimeException { + /** + * + */ + private static final long serialVersionUID = -7143250551383159031L; + public CodeGenerationException(Throwable cause) { super(cause); } 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 022e80a93a..5faeef257c 100644 --- a/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/GenerateMojo.java +++ b/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/GenerateMojo.java @@ -23,6 +23,9 @@ import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.FileUtils; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.LifecyclePhase; import java.io.File; @@ -30,68 +33,70 @@ * A mojo that uses the quickfix code generator to generate * Java source files from a QuickFIX Dictionary. * - * @goal generate - * @phase generate-sources * @description QuickFIX/J code generation plugin - * @author Claudio Bantaloukas + * @author Claudio Bantaloukas */ +@Mojo( name = "generate", defaultPhase = LifecyclePhase.GENERATE_SOURCES ) public class GenerateMojo extends AbstractMojo { /** * The dictionary file to use for mapping messages to java. - * - * @parameter expression="${basedir}/src/main/quickfixj/dictionary/FIX44.xml" */ + @Parameter(defaultValue="${basedir}/src/main/quickfixj/dictionary/FIX44.xml") private File dictFile; /** * The source directory containing *.xsd files. - * - * @parameter expression="${basedir}/src/resources/quickfixj/codegenerator" */ + @Parameter(defaultValue="${basedir}/src/resources/quickfixj/codegenerator") private File schemaDirectory; /** * The directory to output the generated sources to. - * - * @parameter expression="${project.build.directory}/generated-sources/" */ + @Parameter(defaultValue="${project.build.directory}/generated-sources/") private File outputDirectory; /** * Enable BigDecimal representation. - * - * @parameter default-value="false" */ + @Parameter(defaultValue="false") private boolean decimal; /** * Enable orderedFields. - * - * @parameter default-value="false" */ + @Parameter(defaultValue="false") private boolean orderedFields; /** * The package for the generated source. - * - * @parameter */ + @Parameter(required = true) private String packaging; /** * The base field class to use. - * - * @parameter default-value = "quickfix.field" */ + @Parameter(defaultValue = "quickfix.field") private String fieldPackage = "quickfix.field"; + /** + * The default UtcTimestampPrecision to be used during field code generation. + */ + @Parameter(required = false) + private String utcTimestampPrecision; + + /** + * Defines whether the code generator should overwrite existing files with the same name + */ + @Parameter(defaultValue = "true") + private boolean overwrite = true; + /** * The Maven project to act upon. - * - * @parameter expression="${project}" - * @required */ + @Parameter(defaultValue = "${project}", required = true) private MavenProject project; /** @@ -135,7 +140,8 @@ public void execute() throws MojoExecutionException { task.setMessagePackage(packaging); task.setOutputBaseDirectory(outputDirectory); task.setFieldPackage(fieldPackage); - task.setOverwrite(true); + task.setUtcTimestampPrecision(utcTimestampPrecision); + task.setOverwrite(overwrite); task.setOrderedFields(orderedFields); task.setDecimalGenerated(decimal); generator.generate(task); @@ -300,4 +306,36 @@ public String getFieldPackage() { public void setFieldPackage(String fieldPackage) { this.fieldPackage = fieldPackage; } + + /** + * Returns the default UtcTimestampPrecision to be used during field code generation. + * + * @return the default UtcTimestampPrecision to be used during field code generation. + */ + public String getUtcTimestampPrecision() { + return utcTimestampPrecision; + } + + /** + * Sets the default UtcTimestampPrecision to be used during field code generation. + * + * @param utcTimestampPrecision the default UtcTimestampPrecision to be used during field code generation. + */ + public void setUtcTimestampPrecision(String utcTimestampPrecision) { + this.utcTimestampPrecision = utcTimestampPrecision; + } + + /** + * @return whether the code generator should overwrite existing files with the same name + */ + public boolean isOverwrite() { + return overwrite; + } + + /** + * @param overwrite sets whether the code generator should overwrite existing files with the same name + */ + public void setOverwrite(boolean overwrite) { + this.overwrite = overwrite; + } } 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 44d0b00cd6..615e639661 100644 --- a/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/MessageCodeGenerator.java +++ b/quickfixj-codegenerator/src/main/java/org/quickfixj/codegenerator/MessageCodeGenerator.java @@ -43,9 +43,15 @@ import java.io.PrintStream; import java.text.DecimalFormat; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; + +import javax.xml.XMLConstants; /** * Generates Message and Field related code for the various FIX versions. @@ -55,6 +61,7 @@ public class MessageCodeGenerator { private static final String BIGDECIMAL_TYPE_OPTION = "generator.decimal"; private static final String ORDERED_FIELDS_OPTION = "generator.orderedFields"; private static final String OVERWRITE_OPTION = "generator.overwrite"; + private static final String UTC_TIMESTAMP_PRECISION_OPTION = "generator.utcTimestampPrecision"; // An arbitrary serial UID which will have to be changed when messages and fields won't be compatible with next versions in terms // of java serialization. @@ -66,6 +73,9 @@ public class MessageCodeGenerator { // The name of the param in the .xsl files to pass the serialVersionUID private static final String XSLPARAM_SERIAL_UID = "serialVersionUID"; + private static final Set UTC_TIMESTAMP_PRECISION_ALLOWED_VALUES = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList("SECONDS", "MILLIS", "MICROS", "NANOS"))); + protected void logInfo(String msg) { System.out.println(msg); } @@ -128,11 +138,24 @@ private void generateFieldClasses(Task task) throws ParserConfigurationException Transformer transformer = createTransformer(task, "Fields.xsl"); for (String fieldName : fieldNames) { String outputFile = outputDirectory + fieldName + ".java"; - if (!new File(outputFile).exists()) { + if (!new File(outputFile).exists() || task.isOverwrite()) { // if overwrite is set, then transform and generate the file logDebug("field: " + fieldName); Map parameters = new HashMap<>(); parameters.put("fieldName", fieldName); parameters.put("fieldPackage", task.getFieldPackage()); + String utcTimestampPrecision = task.getUtcTimestampPrecision(); + if (utcTimestampPrecision != null) { + String utcTimestampPrecisionParameterName = "utcTimestampPrecision"; + if (!UTC_TIMESTAMP_PRECISION_ALLOWED_VALUES.contains(utcTimestampPrecision)) { + throw new CodeGenerationException(new IllegalArgumentException(String.format( + "Allowed values for parameter %s are %s. Supplied value: \"%s\".", + utcTimestampPrecisionParameterName, + String.join(", ", UTC_TIMESTAMP_PRECISION_ALLOWED_VALUES), + utcTimestampPrecision + ))); + } + parameters.put(utcTimestampPrecisionParameterName, utcTimestampPrecision); + } if (task.isDecimalGenerated()) { parameters.put("decimalType", "java.math.BigDecimal"); parameters.put("decimalConverter", "Decimal"); @@ -218,6 +241,8 @@ private Document getSpecification(Task task) throws ParserConfigurationException Document document = specificationCache.get(task.getName()); if (document == null) { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); DocumentBuilder builder = factory.newDocumentBuilder(); document = builder.parse(task.getSpecification()); specificationCache.put(task.getName(), document); @@ -280,13 +305,7 @@ private void generateCodeFile(Task task, Document document, Map if (!task.isOverwrite()) { return; } - if (outputFile.lastModified() > task.getSpecificationLastModified()) { - logDebug("Skipping file " + outputFile.getName()); - return; - } } - logDebug("spec has mod " + task.getSpecificationLastModified() + - " output has mod " + outputFile.lastModified()); DOMSource source = new DOMSource(document); FileOutputStream fos = new FileOutputStream(outputFile); @@ -327,6 +346,7 @@ public static class Task { private File outputBaseDirectory; private String messagePackage; private String fieldPackage; + private String utcTimestampPrecision; private boolean overwrite = true; private File transformDirectory; private boolean orderedFields; @@ -369,6 +389,14 @@ public void setFieldPackage(String fieldPackage) { this.fieldPackage = fieldPackage; } + public String getUtcTimestampPrecision() { + return utcTimestampPrecision; + } + + public void setUtcTimestampPrecision(String utcTimestampPrecision) { + this.utcTimestampPrecision = utcTimestampPrecision; + } + public String getMessageDirectory() { return messagePackage.replace('.', '/'); } @@ -428,6 +456,7 @@ public static void main(String[] args) { return; } + String utcTimestampPrecision = getOption(UTC_TIMESTAMP_PRECISION_OPTION, null); boolean overwrite = getOption(OVERWRITE_OPTION, true); boolean orderedFields = getOption(ORDERED_FIELDS_OPTION, false); boolean useDecimal = getOption(BIGDECIMAL_TYPE_OPTION, false); @@ -444,6 +473,7 @@ public static void main(String[] args) { task.setMessagePackage("quickfix." + version.toLowerCase()); task.setOutputBaseDirectory(new File(args[2])); task.setFieldPackage("quickfix.field"); + task.setUtcTimestampPrecision(utcTimestampPrecision); task.setOverwrite(overwrite); task.setOrderedFields(orderedFields); task.setDecimalGenerated(useDecimal); @@ -459,6 +489,11 @@ public static void main(String[] args) { } } + @SuppressWarnings("SameParameterValue") + private static String getOption(String key, String defaultValue) { + return System.getProperties().getProperty(key, defaultValue); + } + private static boolean getOption(String key, boolean defaultValue) { return System.getProperties().containsKey(key) ? Boolean.getBoolean(key) : defaultValue; } 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 1f2228fb51..d7eeae25cd 100644 --- a/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/Fields.xsl +++ b/quickfixj-codegenerator/src/main/resources/org/quickfixj/codegenerator/Fields.xsl @@ -22,6 +22,7 @@ + double Double @@ -60,35 +61,42 @@ package ; import quickfix.Field; + +import quickfix.UtcTimestampPrecision; + -import java.time.LocalDateTime; +import java.time.LocalDateTime; + -import java.time.LocalDate; - -import java.time.LocalTime; - +import java.time.LocalDate; + + +import java.time.LocalTime; + public class extends Field { - static final long serialVersionUID = ; - - public static final int FIELD = ; - - public () { - super(); - } - - public ( data) { - super(, data, true); - } - - - - public (double data) { - super(, new (data)); - } - -} - + static final long serialVersionUID = ; + + public static final int FIELD = ; + + public () { + super(); + } + + public ( data) { + super(, data, true); + } + + public (double data) { + super(, new (data)); + } + + + @Override + protected UtcTimestampPrecision getDefaultUtcTimestampPrecision() { + return UtcTimestampPrecision.; + } +} @@ -100,9 +108,12 @@ public class extends String LocalDateTime + LocalTime LocalTime LocalDate LocalDate + String + String boolean double @@ -127,9 +138,12 @@ public class extends String UtcTimeStamp + UtcTime UtcTimeOnly UtcDateOnly UtcDateOnly + String + String Boolean Double @@ -167,32 +181,32 @@ public class extends - 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 int = ; - - public static final int = ; - - public static final String = ""; - - public static final String = ""; - - public static final char = ''; - + + public static final int = ; + + public static final int = ; + + public static final String = ""; + + public static final String = ""; + + public static final char = ''; + - public static final String OPTION = "OPT"; + public static final String OPTION = "OPT"; - public static final String FUTURE = "FUT"; + public static final String FUTURE = "FUT"; diff --git a/quickfixj-codegenerator/src/test/java/org/quickfixj/codegenerator/OverwriteTest.java b/quickfixj-codegenerator/src/test/java/org/quickfixj/codegenerator/OverwriteTest.java new file mode 100644 index 0000000000..a5358b071f --- /dev/null +++ b/quickfixj-codegenerator/src/test/java/org/quickfixj/codegenerator/OverwriteTest.java @@ -0,0 +1,135 @@ +package org.quickfixj.codegenerator; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Scanner; + +import org.apache.maven.plugin.MojoExecutionException; +import org.junit.Before; +import org.junit.Test; +import org.apache.commons.io.FileUtils; + +public class OverwriteTest { + + private File outputDirectory = new File ("./target/test-output/"); + private File dictDirectory = new File ("./src/test/resources"); + private File schemaDirectory = new File ("./src/main/resources/org/quickfixj/codegenerator"); + private String fieldPackage = "quickfix.field"; + private String utcTimestampPrecision = null; + private boolean orderedFields = true; + private boolean decimal = true; + private MessageCodeGenerator generator; + + @Before + public void setup() throws IOException { + if (outputDirectory.exists()){ + FileUtils.cleanDirectory(outputDirectory); + } else { + outputDirectory.mkdirs(); + } + generator = new MessageCodeGenerator(); + System.out.println("Successfully created an instance of the QuickFIX source generator"); + } + + @Test + public void testFieldOverwrittenWhenOverwriteTrue() { + + boolean overwrite = true; + + MessageCodeGenerator.Task task = new MessageCodeGenerator.Task(); + System.out.println("Initialising code generator task"); + + try { + String packaging = "quickfix.fix41"; + File fix41Dictfile = new File( dictDirectory, "FIX41.xml" ); + generate(generator, task, fix41Dictfile, packaging, overwrite); + + packaging = "quickfix.fix42"; // this does not affect this test + File fix42Dictfile = new File( dictDirectory, "FIX42.xml" ); + generate(generator, task, fix42Dictfile, packaging, overwrite); + } catch (MojoExecutionException e) { + e.printStackTrace(); + fail(); + } + + String expectedFilePath = outputDirectory.getAbsolutePath() + "/quickfix/field/AllocShares.java"; + File file = new File(expectedFilePath); + assertTrue(file.exists()); + + boolean isAllocSharesDecimal = isAllocSharesDecimal(file); + assertTrue(isAllocSharesDecimal); + } + + @Test + public void testFieldNotOverwrittenWhenOverwriteFalse() { + + boolean overwrite = false; + + MessageCodeGenerator.Task task = new MessageCodeGenerator.Task(); + System.out.println("Initialising code generator task"); + + try { + String packaging = "quickfix.fix41"; + File fix41Dictfile = new File( dictDirectory, "FIX41.xml" ); + generate(generator, task, fix41Dictfile, packaging, overwrite); + + packaging = "quickfix.fix42"; // this does not affect this test + File fix42Dictfile = new File( dictDirectory, "FIX42.xml" ); + generate(generator, task, fix42Dictfile, packaging, overwrite); + } catch (MojoExecutionException e) { + e.printStackTrace(); + fail(); + } + + String expectedFilePath = outputDirectory.getAbsolutePath() + "/quickfix/field/AllocShares.java"; + File file = new File(expectedFilePath); + assertTrue(file.exists()); + + boolean isAllocSharesDecimal = isAllocSharesDecimal(file); + assertFalse(isAllocSharesDecimal); + } + + private void generate(MessageCodeGenerator generator, MessageCodeGenerator.Task task, File dictfile, + String packaging, boolean overwrite) throws MojoExecutionException { + if (dictfile != null && dictfile.exists()) { + task.setSpecification(dictfile); + } else { + throw new MojoExecutionException("File could not be found or was NULL!"); + } + + System.out.println("Processing " + dictfile); + + task.setName(dictfile.getName()); + task.setTransformDirectory(schemaDirectory); + task.setMessagePackage(packaging); + task.setOutputBaseDirectory(outputDirectory); + task.setFieldPackage(fieldPackage); + task.setUtcTimestampPrecision(utcTimestampPrecision); + task.setOverwrite(overwrite); + task.setOrderedFields(orderedFields); + task.setDecimalGenerated(decimal); + generator.generate(task); + } + + private boolean isAllocSharesDecimal(File file) { + boolean isAllocSharesDecimal = false; + try (Scanner scanner = new Scanner(file)) { + //now read the file line by line... + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if(line.contains("AllocShares extends DecimalField")) { + isAllocSharesDecimal = true; + break; + } + } + } catch(FileNotFoundException e) { + e.printStackTrace(); + fail(); + } + return isAllocSharesDecimal; + } + +} diff --git a/quickfixj-codegenerator/src/test/resources/FIX41.xml b/quickfixj-codegenerator/src/test/resources/FIX41.xml new file mode 100644 index 0000000000..4a328f079b --- /dev/null +++ b/quickfixj-codegenerator/src/test/resources/FIX41.xml @@ -0,0 +1,36 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
diff --git a/quickfixj-codegenerator/src/test/resources/FIX42.xml b/quickfixj-codegenerator/src/test/resources/FIX42.xml new file mode 100644 index 0000000000..d48a87328f --- /dev/null +++ b/quickfixj-codegenerator/src/test/resources/FIX42.xml @@ -0,0 +1,41 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
diff --git a/quickfixj-core/pom.xml b/quickfixj-core/pom.xml index 12debac300..b7aafd9eae 100644 --- a/quickfixj-core/pom.xml +++ b/quickfixj-core/pom.xml @@ -1,440 +1,330 @@ - 4.0.0 - - org.quickfixj - quickfixj-parent - 2.2.0-SNAPSHOT - + 4.0.0 + + org.quickfixj + quickfixj-parent + 3.0.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 - + + + org.quickfixj + quickfixj-base + ${project.version} + + + org.quickfixj + quickfixj-messages-fixlatest + ${project.version} + test + + + org.quickfixj + quickfixj-messages-fix50sp2 + ${project.version} + test + + + org.quickfixj + quickfixj-messages-fix50sp1 + ${project.version} + test + + + org.quickfixj + quickfixj-messages-fix50 + ${project.version} + test + + + org.quickfixj + quickfixj-messages-fix44 + ${project.version} + test + + + org.quickfixj + quickfixj-messages-fix43 + ${project.version} + test + + + org.quickfixj + quickfixj-messages-fix42 + ${project.version} + test + + + org.quickfixj + quickfixj-messages-fix41 + ${project.version} + test + + + org.quickfixj + quickfixj-messages-fix40 + ${project.version} + test + + + org.quickfixj + quickfixj-messages-fixt11 + ${project.version} + test + + + org.junit.vintage + junit-vintage-engine + test + + + org.mockito + mockito-core + test + + + org.hamcrest + hamcrest + 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 + + + org.slf4j + slf4j-api + ${slf4j.version} + + + com.sleepycat + je + 18.3.12 + true + + + com.zaxxer + HikariCP + 4.0.3 + + - - org.apache.mina - mina-core - 2.1.3 - - - 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 - - + + + - - - - ../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/test/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.apache.maven.plugins + maven-jar-plugin + + + quickfix/** + org/** + quickfix/field/* + quickfix/field/converter/* + FIX*.xml + + + + quickfix/fix*/** + + + + + org.apache.maven.plugins + maven-source-plugin + + + quickfix/** + org/** + quickfix/field/* + quickfix/field/converter/* + FIX*.xml + + + + quickfix/fix*/** + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + true + 8 + src/main/java:${project.build.directory}/generated-sources + + + + + + org.apache.felix + maven-bundle-plugin + + + quickfix,quickfix.*,org.quickfixj,org.quickfixj.* + + + + + + - - - org.quickfixj - quickfixj-codegenerator - ${project.version} - - - fixt11 - - generate - - - ../quickfixj-messages/quickfixj-messages-fixt11/src/main/resources/FIXT11.xml - quickfix.fixt11 - 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} - - - - 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} - - - - - - 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 - - - - - + + + + org.apache.maven.plugins + maven-surefire-report-plugin + ${maven-surefire-plugin-version} + + + true + + + + org.apache.maven.plugins + maven-jxr-plugin + 3.3.1 + + + - - - - maven-surefire-report-plugin - ${maven-surefire-plugin-version} - - - true - - - - maven-jxr-plugin - 2.5 - - - - - - - - skipAT - - - skipAT - true - - - - - - - + + + + skipAT + + + skipAT + true + + + + + + + + + surefire-java8 + + 1.8 + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + -Xmx2g -Djava.net.preferIPv4Stack=true + + false + + **/*Test.java + ${acceptance.tests} + + + **/*ForTest.java + **/Abstract*Test.java + **/AcceptanceTestSuite$* + + + 5 + 60000 + 5 + false + + + + + + + + + surefire + + [1.9,) + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + -Xmx2g -Djava.net.preferIPv4Stack=true + --add-opens java.base/java.lang=ALL-UNNAMED + + false + + **/*Test.java + ${acceptance.tests} + + + **/*ForTest.java + **/Abstract*Test.java + **/AcceptanceTestSuite$* + + + 5 + 60000 + 5 + false + + + + + + + diff --git a/quickfixj-core/readme.md b/quickfixj-core/readme.md new file mode 100644 index 0000000000..e1a31e128c --- /dev/null +++ b/quickfixj-core/readme.md @@ -0,0 +1,13 @@ +# quickfixj-core + +This module builds the QuickFIX/J FIX "engine" including supporting capabilities. + +The core engine depends on ```quickfixj-base```. A small number of derived Fields are provided by ```quickfixj-base``. + + This module has **test** dependency on generated message classes. The distinction of *test* dependency is significant as it allows easier [customisation](../customising-quickfixj.md) of QuickFIX/J deployments. + +## Developing quickfixj-core + +To develop this module build ```quickfixj-base``` and ```quickfixj-messages``` first, or build the whole project. + +Full ```quickfixj-messages``` build times can be long. See [the ```quickfixj-messages``` readme](../quickfixj-messages/readme.md) for how to do a minimal development build of the messages. Once these dependencies are built then ```quickfixj-core``` can be built and tested independently for higher productivity. diff --git a/quickfixj-core/src/main/doc/usermanual/installation.html b/quickfixj-core/src/main/doc/usermanual/installation.html index 500f09e8d4..b9acb4c2b9 100644 --- a/quickfixj-core/src/main/doc/usermanual/installation.html +++ b/quickfixj-core/src/main/doc/usermanual/installation.html @@ -13,39 +13,77 @@

QuickFIX/J User Manual

Installation and Building the Code.

Runtime Dependencies

Java Virtual Machine:

-

JVM compatible with Oracle JRE Java 1.7.x or higher.

+

JVM compatible with JRE Java 1.8.x or higher.

Required run-time libraries:

+

QuickFIX/J has "base" and "core" libraries for the FIX "Engine" and one or more Message libraries are required at runtime.

+ +

The required Message libraries depend on the FIX Protocol Version. A QuickFIX/J process can support more than one FIX Protocol version at runtime, using different Session configurations.

+ +

Please note that customised versions of the FIX message artefacts can be built to meet specific Rules of Engagement. Please refer to the QuickFIX/J github repository for details.

+

-(Note: The actual JAR files may have version numbers in them.) +(Note: The actual JAR files have semantic version numbers in the name. For example : quickfixj-messages-fixlatest-3.0.0.jar)

- - - - - - - - + + + + + + + + + + + + + + + + +
LibraryDescription
The QFJ core JAR and message JARs. -
    -
  • quickfixj-core.jar -
  • quickfixj-msg-fix40.jar -
  • quickfixj-msg-fix41.jar -
  • quickfixj-msg-fix42.jar -
  • quickfixj-msg-fix43.jar -
  • quickfixj-msg-fix44.jar -
  • quickfixj-msg-fix50.jar -
  • quickfixj-msg-fix50sp1.jar -
  • quickfixj-msg-fix50sp2.jar -
- or -
    -
  • quickfixj-all.jar (includes core and message JARs) -
-
QFJ runtime libraries
LibraryDescription
The QuickFIX/J base and core JARs and message JARs. +
    +
  • quickfixj-base-{VERSION}.jar +
  • quickfixj-core-{VERSION}.jar +
+
QuickFIX/J fundamental libraries
With one or more message JAR for FIX versions before FIX 5.0 +
    +
  • quickfixj-messages-fix40-{VERSION}.jar +
  • quickfixj-messages-fix41-{VERSION}.jar +
  • quickfixj-messages-fix42-{VERSION}.jar +
  • quickfixj-messages-fix43-{VERSION}.jar +
  • quickfixj-messages-fix44-{VERSION}.jar +
+ AND/OR + + + + + + + +
+ The FIX Transport Layer JAR +
    +
  • quickfixj-messages-fixt11-{VERSION}.jar
  • +
+
+ With one or more FIX Application Layer message JAR for FIX versions 5.0 and later +
    +
  • quickfixj-messages-fix50-{VERSION}.jar +
  • quickfixj-messages-fix50sp1-{VERSION}.jar +
  • quickfixj-messages-fix50sp2-{VERSION}.jar +
  • quickfixj-messages-fixlatest-{VERSION}.jar +
+
+
Message Libraries required at runtime
OR
+
    +
  • quickfixj-all-{VERSION}.jar (includes core, base and message JARs) +
+
JAR including base, core and message JARs

@@ -81,16 +119,10 @@

Optional run-time libraries:

use Log4J logging. - proxool.jar + HikariCP.jar This JAR provided database connection pooling capabilities. It is required if you are using the JDBC store or log. - - jcl104-over-slf4j.jar - Adapts Jakarta Commons Logging to SLF4J. Required if you are using an optional - library that depends on Jakarta Commons Logging. Currently, this includes Proxool - (needed by JDBC store and log for connection pooling). - sleepycat-je.jar Needed if the SleepyCat JE message store is used. @@ -108,12 +140,11 @@

Optional run-time libraries:

Building QuickFIX/J

These instructions are for developers who don't want - to use the prebuilt binaries or are intending to modify and rebuild the QuickFIX/J + to use the pre-built binaries or are intending to modify and rebuild the QuickFIX/J code. If you are building the code from the command line you'll need - to download and install Maven (version 3.2.5 or newer). If you are building from - an IDE, Maven is usually included. + to download and install Maven (version 3.5.0 or newer). -Building from source requires Java 7+. +Building from source requires Java 8 or later.

    @@ -122,7 +153,7 @@

    Building QuickFIX/J

    GitHub for more details on cloning the repository.
  1. Change directory to the top-level directory of the checked out code. You should see a pom.xml file.
  2. -
  3. Run mvn package to build the QuickFIX/J and examples jar files. +
  4. Run mvnw package to build the QuickFIX/J and examples jar files. This will also generate all the FIX message-related code for the various FIX versions.
  5. There is an option for the code generator to use BigDecimal instead of double for fields like price and quantity. @@ -130,7 +161,7 @@

    Building QuickFIX/J

    running the generate.code target.

Command-line Switches

-There are various command-line switches you can pass to ant to modify the produced behavior: +There are various command-line switches you can pass to mvnw to modify the produced behavior: @@ -150,66 +181,76 @@

Command-line Switches

Switch

For example, in order to generate fields with BigDecimals and skip acceptance tests:
-mvn test -Dgenerator.decimal=true -DskipAT=true +mvnw test -Dgenerator.decimal=true -DskipAT=true

+

Find further details in the project readme

IDE support:

-

There are - Eclipse and Netbeans project definition files in the top-level directory of the checked out directory. - - When the project is first created, it will not have the generated message classes +

When the project is first created, it will not have the generated message classes and compile errors will occur! Best is to compile once on the command line before importing the project into the IDE. + If the IDE reports some errors after the compilation with mvnw package, try to use mvnw install . +

Maven Integration:

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

-

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

+

You need only include message libraries for the FIX protocol version that you require.

+

Note : the quickfixj-messages-fixt11 jar is required for FIX versions 5.0 and later.

+

The following example shows maven dependencies, any SLF4J compatible logging implementation can be used:
+

 <!-- QuickFIX/J dependencies -->
 <dependency>
     <groupId>org.quickfixj</groupId>
     <artifactId>quickfixj-core</artifactId>
-    <version>2.0.0</version>
+    <version>3.0.0</version>
+</dependency>
+<dependency>
+    <groupId>org.quickfixj</groupId>
+    <artifactId>quickfixj-base</artifactId>
+    <version>3.0.0</version>
 </dependency>
 <dependency>
     <groupId>org.quickfixj</groupId>
     <artifactId>quickfixj-messages-fix40</artifactId>
-    <version>2.0.0</version>
+    <version>3.0.0</version>
 </dependency>
 <dependency>
     <groupId>org.quickfixj</groupId>
     <artifactId>quickfixj-messages-fix41</artifactId>
-    <version>2.0.0</version>
+    <version>3.0.0</version>
 </dependency>
 <dependency>
     <groupId>org.quickfixj</groupId>
     <artifactId>quickfixj-messages-fix42</artifactId>
-    <version>2.0.0</version>
+    <version>3.0.0</version>
 </dependency>
 <dependency>
     <groupId>org.quickfixj</groupId>
     <artifactId>quickfixj-messages-fix43</artifactId>
-    <version>2.0.0</version>
+    <version>3.0.0</version>
 </dependency>
 <dependency>
     <groupId>org.quickfixj</groupId>
     <artifactId>quickfixj-messages-fix44</artifactId>
-    <version>2.0.0</version>
+    <version>3.0.0</version>
 </dependency>
 <dependency>
     <groupId>org.slf4j</groupId>
-    <artifactId>slf4j-log4j12</artifactId>
-    <version>1.7.22</version>
+    <artifactId>slf4j-api</artifactId>
+    <version>2.0.6</version>
 </dependency>
 <dependency>
     <groupId>org.slf4j</groupId>
-    <artifactId>slf4j-api</artifactId>
-    <version>1.7.22</version>
-</dependency>
+ <artifactId>slf4j-log4j12</artifactId> + <version>2.0.6/version> +</dependency> + +

+

Please refer to the QuickFIX/J github repository for more details about working with QuickFIX/J.

+

Generating the database for JDBC based store and log

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. @@ -220,7 +261,7 @@

Generating the database for JDBC based store and log< 'root' to another user. To add a password, use pass the -p option after the username. Similar scripts are provided for MSSQL, PostgreSQL and Oracle.

-

Special notes for Oracle

+

Special notes for Oracle

Oracle treats empty strings as null values. Null values are not allowed for primary key fields. The fields used in the primary keys are: diff --git a/quickfixj-core/src/main/doc/usermanual/support.html b/quickfixj-core/src/main/doc/usermanual/support.html index caec340aa2..0fc87ec45c 100644 --- a/quickfixj-core/src/main/doc/usermanual/support.html +++ b/quickfixj-core/src/main/doc/usermanual/support.html @@ -13,7 +13,7 @@

QuickFIX/J User Manual

QuickFIX/J Support

Mailing Lists

-

QuickFIX/J issues can be discussed on the mailing +

QuickFIX/J issues can be discussed on the mailing lists hosts at the SourceForge project site.

Web Site

@@ -21,9 +21,11 @@

Web Site

site.

More information about the original C++ QuickFIX can be found at the quickfixengine.org web site.

+

Git Hub

+

Further detailed documents and source code can be found on github

FIX Protocol

-

More information about the FIX protocol can be found at the FIX - Protocol Limited web site.

+

More information about the FIX protocol can be found at the FIX + Trading Community web site.

diff --git a/quickfixj-core/src/main/doc/usermanual/usage/acceptor_failover.html b/quickfixj-core/src/main/doc/usermanual/usage/acceptor_failover.html index aaf634963b..fac4716cde 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/acceptor_failover.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/acceptor_failover.html @@ -9,10 +9,10 @@

QuickFIX/J User Manual

Simple Failover for Socket-based Acceptors.

- When using a MessageStore that supports shared data (FileStore, JdbcStore, - SleepycatStore) and that implements the new RefreshableMessageStore interface, + When using a MessageStore that supports shared data (FileStore, JdbcStore and SleepycatStore), the Session can be configured to refresh the store information upon - logon. You would typically run two acceptor processes using a + logon with configuration RefreshOnLogon=Y. + You would typically run two acceptor processes using a shared message store. One process would be the active acceptor and the other would be the standby for any specific session. If one acceptor process dies, the client (assuming they have been @@ -33,7 +33,7 @@

Simple Failover for Socket-based Acceptors.

EndTime=00:00:00 HeartBtInt=30 SocketAcceptPort=9877 -RefreshMessageStoreAtLogon=Y +RefreshOnLogon=Y [session] SenderCompID=EXEC diff --git a/quickfixj-core/src/main/doc/usermanual/usage/application.html b/quickfixj-core/src/main/doc/usermanual/usage/application.html index 760714fab1..00531cc996 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/application.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/application.html @@ -52,11 +52,11 @@

Creating Your QuickFIX/J Application

This could happen during a normal logout exchange or because of a forced termination or a loss of network connection.
toAdmin
-
This callback provides you with a peak at the administrative messages +
This callback provides you with a peek at the administrative messages that are being sent from your FIX engine to the counter party. This is normally not useful for an application however it is provided for any logging you may wish to do. Notice that the FIX::Message is not const. This allows you to add - fields before an adminstrative message before it is sent out.
+ fields to an administrative message before it is sent out.
toApp
This is a callback for application messages that you are being sent to a counterparty. If you throw a DoNotSend exception in this function, diff --git a/quickfixj-core/src/main/doc/usermanual/usage/codegen.html b/quickfixj-core/src/main/doc/usermanual/usage/codegen.html index fcb833bb24..2191667a04 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/codegen.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/codegen.html @@ -9,82 +9,91 @@

QuickFIX/J User Manual

Customizing Message Code Generation

-QuickFIX/J includes message libraries generated from standard FIX meta data. However, -there are several ways to customize code generation in QuickFIX/J. -

-

-The simplest customization is to just modify one or more data dictionaries -(e.g., FIX44.xml) and rebuild QFJ. This allows you to add custom fields, -define new messages not included in the specification, change whether fields -are required or optional, and so on. -

-

-You can also do more advanced customization by using the -quickfix.codegen.MessageCodeGenerator class. You can define -code generation tasks (quickfix.codegen.MessageCodeGenerator.Task) -that customize various aspects of the generation -process. The types of customizations currently supported include: -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PropertyDescription
specificationpath to the XML file containing the FIX meta data (e.g. /my/dir/CUSTOM_FIX.xml)
transformDirectorypath to the XSLT transforms used to generate the message-related source code. Usually this - will reference the standard QFJ XSLT templates in quickfix/codegen. However, you can - modify the templates and put the modified templates wherever you want.
outputBaseDirectoryThe base directory where generated source code will be placed.
overwriteControls whether existings files are overwritten. Usually this would be true.
messagePackageThe Java package for the generated messages. This would be something like "my.message.fix42". - In QFJ, for example, the package for the FIX 4.2 generated messages is "quickfix.fix42".
fieldPackageThe Java package for the generated field. This would be something like "my.message.fix42.fields". - In QFJ, the fields are generated in the package "quickfix.fields" for all versions (newer versions - overwrite older versions). By changing this property, you could generate fields into separate packages - for each specification version.
orderedFieldsGenerates message classes where regular (nongroup) body fields are ordered as specified in - the meta. Although the FIX specification does not require this ordering, some exchanges do - require it. There may be a slight (probably very slight) performance degradation when - using this option.
decimalGeneratedGenerates BigDecimals for price, quantity, and similar fields. The default code - generation generated doubles to be compatible with the QuickFIX C++ implementation.
-

-To generate you own message library, you can create a program that uses the MessageCodeGenerator -to process the Task you have defined. Then use a simple Ant script to do the code -generation, compile the classes, and create a message JAR file. Depending on the type of customization -you've done, you may need to write a MessageFactory implementation, include that in the -JAR file, and then reference the factory in the session configuration. For Ant script examples, see -the QFJ core build scripts. -

-The most advanced customization is to modify the XSLT templates used to generate the -message source code. If you are interested in this level of customization, please look -at the XSLT template source code for more information. +QuickFIX/J includes message libraries generated from FIX standard meta-data. However, it is very common to need to +customize the FIX Protocol to suit an organisations specific "Rules of Engagement". +There is more than one way to customize Message and Field code generation in QuickFIX/J.

+
    +
  • For FIX versions prior to "FIX Latest" : +
      +
    • You may fork the entire QuickFIX/J repository and modify one or more data dictionaries (e.g., FIX44.xml) and rebuild QFJ. + This allows you to add custom Fields, define new Messages not included in the specification, change whether Fields are required or optional, and so on. + This approach is good for prototyping and/or experimentation but is not recommended in the middle to long term due to the disadvantage of needing to maintain a fork of the QuickFIX/J project.
    • +
    • You may create an independent project to build only your QuickFIX/J Messages and Fields using the tools provided by + the QuickFIX/J project. See the readme in the QuickFIX/J Messages module (quickfixj-messages) for further details. + This allows you to manage dependencies on the QuickFIX/J core runtime independent of the custom Message and Field packages + (within the scope of binary compatible QuickFIX/J major releases).
    • +
    +

    You can also do more advanced customization by using the + quickfix.codegen.MessageCodeGenerator class. You can define + code generation tasks (quickfix.codegen.MessageCodeGenerator.Task) + that customize various aspects of the generation + process. The types of customizations currently supported include: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PropertyDescription
    specificationpath to the XML file containing the FIX meta data (e.g. /my/dir/CUSTOM_FIX.xml)
    transformDirectorypath to the XSLT transforms used to generate the message-related source code. Usually this + will reference the standard QFJ XSLT templates in quickfix/codegen. However, you can + modify the templates and put the modified templates wherever you want.
    outputBaseDirectoryThe base directory where generated source code will be placed.
    overwriteControls whether existings files are overwritten. Usually this would be true.
    messagePackageThe Java package for the generated messages. This would be something like "my.message.fix42". + In QFJ, for example, the package for the FIX 4.2 generated messages is "quickfix.fix42".
    fieldPackageThe Java package for the generated field. This would be something like "my.message.fix42.fields". + In QFJ, the fields are generated in the package "quickfix.fields" for all versions (newer versions + overwrite older versions). By changing this property, you could generate fields into separate packages + for each specification version.
    orderedFieldsGenerates message classes where regular (nongroup) body fields are ordered as specified in + the meta. Although the FIX specification does not require this ordering, some exchanges do + require it. There may be a slight (probably very slight) performance degradation when + using this option.
    decimalGeneratedGenerates BigDecimals for price, quantity, and similar fields. The default code + generation generated doubles to be compatible with the QuickFIX C++ implementation.
    utcTimestampPrecisionThe default UtcTimestampPrecision to be used during field code generation.
    +

    The most complex customization for this case is to modify the XSLT templates used to generate the + message source code. If you are interested in this level of customization, please look + at the XSLT template source code for more information.

    +

    You can build a QuickFIX/J Dictionary and generate code directly from a FIX Orchestra specification + using the new QuickFIX/J project tools. See the readme in the QuickFIX/J Messages module (quickfixj-messages) for further details.

    +
  • +
  • For FIX Latest: +

    It is recommended to create an independent project to build only your QuickFIX/J Messages and Fields using the tools provided by + the QuickFIX/J project. See the readme in the QuickFIX/J Messages module (quickfixj-messages) for further details. + This allows you to manage dependencies on the QuickFIX/J core runtime independent of the custom Message and Field packages + (within the scope of binary compatible QuickFIX/J major releases).

    +
  • +
diff --git a/quickfixj-core/src/main/doc/usermanual/usage/configuration.html b/quickfixj-core/src/main/doc/usermanual/usage/configuration.html index fb2ac39076..da01eb46e2 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/configuration.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/configuration.html @@ -283,7 +283,7 @@

QuickFIX Settings

Valid XML data dictionary file, QuickFIX/J comes with the following defaults in the etc directory: FIXT11.xml, FIX50.xml, FIX44.xml, FIX43.xml, FIX42.xml, FIX41.xml, FIX40.xml. - If DataDictionary is not specified and UserDataDictionary=Y, then QuickFIX/J will look for a + If DataDictionary is not specified and UseDataDictionary=Y, then QuickFIX/J will look for a default dictionary based on the session's BeginString (e.g., FIX.4.2 = FIX42.xml). The DataDictionary file search strategy is to use a URL, then the file system, and then the thread context classloader (if any), and then the DataDictionary instance's classloader. Default data dictionary files @@ -528,23 +528,31 @@

QuickFIX Settings

  - SocketConnectHost<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> -

+ + valid IP address in the format of x.x.x.x or a domain name + +   + + + SocketConnectProtocol<n> + Alternate socket protocol(s) for connecting to a session for failover or load balancing, + where n is a positive integer, i.e. SocketConnectProtocol1, SocketConnectProtocol2, etc. + Must be consecutive and have a matching SocketConnectHost & SocketConnectPort +

Connection list iteration rules:

    -
  • Connections are tried one after another until one is successful: SocketConnectHost:SocketConnectPort, - SocketConnectHost1:SocketConnectPort1, etc.
  • +
  • Connections are tried one after another until one is successful: SocketConnectProtocol - SocketConnectHost:SocketConnectPort, + SocketConnectProtocol1 - 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 - -   + "TCP" or "VM_PIPE". + "TCP" @@ -574,19 +582,14 @@

QuickFIX Settings

SocketAcceptPort - - Socket port for listening to incomming connections, Only used with a SocketAcceptor - positive integer, valid open socket port. Currently, this must be defined in the [DEFAULT] - section. + Socket port for listening to incoming connections. Only used with a SocketAcceptor + positive integer, valid open socket port.   SocketAcceptAddress - Local IP address to for binding accept port. - A hostname or IP address parseable by java.net.InetAddress.
- Currently, this must be defined in the [DEFAULT] section. - + A hostname or IP address parsable by java.net.InetAddress. Accept connections on any network interface. @@ -607,13 +610,9 @@

QuickFIX Settings

empty, ie all remote addresses are allowed - RefreshOnLogon - - Refresh the session state when a logon is received. This allows a simple form of failover - when the message store data is persistent. The - option will be ignored for message stores that are not persistent - (e.g., MemoryStore). - Y
N + AcceptorTemplate + Designates a template Acceptor session. See Dynamic Acceptor Sessions + Y
N N @@ -693,10 +692,12 @@

QuickFIX Settings

Java default cipher suites - UseSNI - Configures the SSL engine to use Server Name Indication. This option is only useful to initiators. - Y
N - N + EndpointIdentificationAlgorithm + Sets the endpoint identification algorithm. If the algorithm parameter is non-null, the endpoint identification/verification procedures must be handled during SSL/TLS handshaking. See + Endpoint Identification + Algorithm Names + + @@ -755,12 +756,10 @@

QuickFIX Settings

Socket Options (Acceptor or Initiator) - Acceptor socket options must be set in settings default section.
Initiator - socket options can be set in either defaults or per-session settings. + Acceptor and Initiator socket options can be set in either defaults or per-session settings. SocketKeepAlive - When the keepalive option is set for a TCP socket and no data has been exchanged across the socket in either direction for @@ -973,8 +972,8 @@

QuickFIX Settings

JdbcDriver JDBC driver for JDBC logger. Also used for JDBC log. Class name for the JDBC driver. Specify driver properties directly will cause the - creation of a Proxool data source that supports connection pooling. If you are using a - database with it's own pooling data source (e.g., Oracle) then use the setDataSource() + creation of a HikariCP data source that supports connection pooling. If you are using a + database with its own pooling data source (e.g., Oracle) then use the setDataSource() method on the Jdbc-related factories to set the data source directly.   @@ -1034,39 +1033,69 @@

QuickFIX Settings

Any nonempty string. "" (empty string) - JdbcMaxActiveConnection Specifies the maximum number of connections to the database. - Any number + Positive number 32 - JdbcMaxActiveTime - Specifies if the housekeeper comes across a thread that has been active for longer than - this (milliseconds) then it will kill it. So make sure you set this to a number bigger than your - slowest expected response! - Any number - 5000 + JdbcMinIdleConnection + Controls the minimum number of idle connections that HikariCP tries to maintain in + the pool, including both idle and in-use connections. If the idle connections dip + below this value, HikariCP will make the best effort to restore them quickly and + efficiently. + + [0, JdbcMaxActiveConnection] + Same as JdbcMaxActiveConnection JdbcMaxConnectionLifeTime Specifies the maximum amount of time that a connection exists for before it is killed (milliseconds). - Any number - 28800000 - - - JdbcSimultaneousBuildThrottle - Specifies the maximum number of connections we can be building at any one time. - That is, the number of new connections that have been requested but aren't yet - available for use. Because connections can be built using more than one thread - (for instance, when they are built on demand) and it takes a finite time between - deciding to build the connection and it becoming available we need some way of - ensuring that a lot of threads don't all decide to build a connection at once. - (We could solve this in a smarter way - and indeed we will one day) - Any number - 32 + Positive + 28800000 ms (8 hours) + + + JdbcConnectionTimeout + Set the maximum number of milliseconds that a client will wait for a connection from the + pool. If this time is exceeded without a connection becoming available, an SQLException + will be thrown from javax.sql.DataSource.getConnection(). + Non-negative number + 250 ms + + + JdbcConnectionIdleTimeout + Controls the maximum amount of time that a connection is allowed to sit idle in the pool. + Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and + average variation of +15 seconds. A connection will never be retired as idle before this timeout. + A value of 0 means that idle connections are never removed from the pool. + Non-negative number + 600000 ms (10 minutes) + + + JdbcConnectionKeepaliveTime + Controls the keepalive interval for a connection in the pool. An in-use connection will never be + tested by the keepalive thread, only when it is idle will it be tested. + Non-negative number + 0 ms + + + JdbcConnectionKeepaliveTime + Controls the keepalive interval for a connection in the pool. An in-use connection will never be + tested by the keepalive thread, only when it is idle will it be tested. + Non-negative number + 0 ms + + + JdbcConnectionTestQuery + Set the SQL query to be executed to test the validity of connections. Using the JDBC4 + Connection.isValid() method to test connection validity can be more efficient on some + databases and is recommended. If your driver supports JDBC4 we strongly recommend not + setting this property. + Valid SQL query + + @@ -1164,7 +1193,7 @@

QuickFIX Settings

    - ScreenLogEvents + ScreenLogShowEvents Log events to screen. Y
N Y @@ -1192,7 +1221,38 @@

QuickFIX Settings

Miscellaneous - + + LogonTag + Tag/value pair which will be set on sent Logon message. + <tag>=<value>, where "tag" has to be a positive integer and "value" a String
+ Example:
+ LogonTag=553=foo + + + + + LogonTag<n> + Additional tag/value pairs which will be set on sent Logon message,
+ where n is a positive integer, i.e. LogonTag1, LogonTag2, etc. + Must be consecutive. + + <tag>=<value>, where "tag" has to be a positive integer and "value" a String
+ Example:
+ LogonTag=553=user
+ LogonTag1=554=password + + + + + RefreshOnLogon + Refresh the session state when a Logon is received. This allows a simple form of failover + when the message store data is persistent. The + option will be ignored for message stores that are not persistent + (e.g., MemoryStore). + Y
N + N + + ResetOnLogon Determines if sequence numbers should be reset before sending/receiving a logon request. Y
N @@ -1202,13 +1262,11 @@

QuickFIX Settings

ResetOnLogout Determines if sequence numbers should be reset to 1 after a normal logout termination. Y
N - N ResetOnDisconnect Determines if sequence numbers should be reset to 1 after an abnormal termination. - Y
N N @@ -1241,7 +1299,7 @@

QuickFIX Settings

ResendRequestChunkSize Setting to limit the size of a resend request in case of missing messages. - This is useful when the remote FIX engine does not allow to ask for more than n message for a ResendRequest. + This is useful when the remote FIX engine does not allow to ask for more than n message for a ResendRequest. It also allows you to prevent a 'self-DDOS' by accidentally requesting a huge flood of messages your system isn't capable of processing if you are substantially behind for some reason.

E.g. if the ResendRequestChunkSize is set to 5 and a gap of 7 messages is detected, a first resend request will be sent for 5 messages. @@ -1252,7 +1310,7 @@

QuickFIX Settings

ContinueInitializationOnError - Continue initializing sessions if an error occurs. + Continue initializing sessions if an error occurs. Useful when having multiple sessions per connector and misconfigured session(s) should not prevent the connector from starting. Y
N N @@ -1265,11 +1323,21 @@

QuickFIX Settings

TestRequestDelayMultiplier Fraction of the heartbeat interval which defines the additional time to wait - if a TestRequest sent after a missing heartbeat times out. + if a TestRequest sent after a missing heartbeat times out (final coefficient value is equal to + TestRequestDelayMultiplier + 1.0). - 0..1 + any non-negative value 0.5 + + HeartBeatTimeoutMultiplier + Fraction of the heartbeat interval which defines the additional time to wait + since the last message was received before disconnecting (final coefficient value is equal to + HeartBeatTimeoutMultiplier + 1.0). + + any non-negative value + 1.4 + DisableHeartBeatCheck Heartbeat detection is disabled. A disconnect due to a missing heartbeat will never occur. @@ -1281,6 +1349,18 @@

QuickFIX Settings

Y
N N + + LogMessageWhenSessionNotFound + Log the entire message when the corresponding session can not be found. Otherwise only the SessionID is logged. + Y
N + Y + + + AllowPosDup + Whether to allow PossDupFlag and OrigSendingTime when sending messages. This is useful on occasions, primarily when a QFJ application is acting as purely a pass-through/monitoring hop. + Y
N + N + 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 c4d48fa243..a9ed58971d 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 @@ -44,6 +44,9 @@ import java.util.List; abstract class ConnectorAdmin implements ConnectorAdminMBean, MBeanRegistration { + + protected static final TabularDataAdapter TABULAR_DATA_ADAPTER = new TabularDataAdapter(); + private final Logger log = LoggerFactory.getLogger(getClass()); public final static String ACCEPTOR_ROLE = "ACCEPTOR"; @@ -52,9 +55,7 @@ abstract class ConnectorAdmin implements ConnectorAdminMBean, MBeanRegistration private final Connector connector; - private static final TabularDataAdapter tabularDataAdapter = new TabularDataAdapter(); - - private final SessionJmxExporter sessionExporter; + protected final SessionJmxExporter sessionExporter; private final JmxExporter jmxExporter; @@ -120,7 +121,7 @@ public TabularData getSessions() throws IOException { sessions.add(new ConnectorSession(session, sessionExporter.getSessionName(sessionID))); } try { - return tabularDataAdapter.fromBeanList("Sessions", "Session", "sessionID", sessions); + return TABULAR_DATA_ADAPTER.fromBeanList("Sessions", "Session", "sessionID", sessions); } catch (OpenDataException e) { throw JmxSupport.toIOException(e); } @@ -134,7 +135,7 @@ public TabularData getLoggedOnSessions() throws OpenDataException { names.add(sessionExporter.getSessionName(sessionID)); } } - return tabularDataAdapter.fromArray("Sessions", "SessionID", toObjectNameArray(names)); + return TABULAR_DATA_ADAPTER.fromArray("Sessions", "SessionID", toObjectNameArray(names)); } private ObjectName[] toObjectNameArray(List sessions) { 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 19a066c6ba..77268bac4f 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 @@ -41,29 +41,22 @@ public class SocketAcceptorAdmin extends ConnectorAdmin implements SocketAccepto private final AbstractSocketAcceptor acceptor; - private static final TabularDataAdapter tabularDataAdapter = new TabularDataAdapter(); - - private final SessionJmxExporter sessionExporter; - public SocketAcceptorAdmin(JmxExporter jmxExporter, AbstractSocketAcceptor connector, ObjectName connectorName, SessionJmxExporter sessionExporter) { super(jmxExporter, connector, connectorName, connector.getSettings(), sessionExporter); - this.sessionExporter = sessionExporter; acceptor = connector; } public static class SessionAcceptorAddressRow { private final SessionID sessionID; - private final SocketAddress acceptorAddress; - private final ObjectName sessionName; - public SessionAcceptorAddressRow(SessionID sessionID, SocketAddress accceptorAddress, + public SessionAcceptorAddressRow(SessionID sessionID, SocketAddress acceptorAddress, ObjectName sessionName) { this.sessionID = sessionID; - this.acceptorAddress = accceptorAddress; + this.acceptorAddress = acceptorAddress; this.sessionName = sessionName; } @@ -82,6 +75,7 @@ public ObjectName getSessionName() { } } + @Override public TabularData getAcceptorAddresses() throws IOException { List rows = new ArrayList<>(); for (Map.Entry entry : acceptor.getAcceptorAddresses().entrySet()) { @@ -90,7 +84,7 @@ public TabularData getAcceptorAddresses() throws IOException { rows.add(new SessionAcceptorAddressRow(sessionID, address, sessionExporter.getSessionName(sessionID))); } try { - return tabularDataAdapter.fromBeanList("AcceptorAddresses", "AddressInfo", "sessionID", + return TABULAR_DATA_ADAPTER.fromBeanList("AcceptorAddresses", "AddressInfo", "sessionID", rows); } catch (OpenDataException e) { throw JmxSupport.toIOException(e); diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketAcceptorAdminMBean.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketAcceptorAdminMBean.java index 58156cd3fb..04bd53248a 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketAcceptorAdminMBean.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketAcceptorAdminMBean.java @@ -22,7 +22,7 @@ import javax.management.openmbean.TabularData; /** - * Management inteface for a socket acceptor connector. + * Management interface for a socket acceptor connector. */ public interface SocketAcceptorAdminMBean extends ConnectorAdminMBean { 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 590e6dfa92..5ba134ebc2 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 @@ -20,19 +20,21 @@ 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.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.net.InetSocketAddress; +import java.net.SocketAddress; import java.util.ArrayList; +import java.util.List; class SocketInitiatorAdmin extends ConnectorAdmin implements SocketInitiatorAdminMBean { - private final TabularDataAdapter tabularDataAdapter = new TabularDataAdapter(); - private final AbstractSocketInitiator initiator; protected SocketInitiatorAdmin(JmxExporter jmxExporter, AbstractSocketInitiator connector, @@ -41,10 +43,18 @@ protected SocketInitiatorAdmin(JmxExporter jmxExporter, AbstractSocketInitiator initiator = connector; } - public TabularData getEndpoints() throws IOException { + + @Override + public TabularData getInitiatorAddresses() throws IOException { + List rows = new ArrayList<>(); + for (IoSessionInitiator initiator : initiator.getInitiators()) { + SessionID sessionID = initiator.getSessionID(); + rows.add(new SessionInitiatorAddressRow(sessionID, initiator.getLocalAddress(), + initiator.getSocketAddresses(), + sessionExporter.getSessionName(sessionID))); + } try { - return tabularDataAdapter.fromBeanList("Endpoints", "Endpoint", "sessionID", - new ArrayList<>(initiator.getInitiators())); + return TABULAR_DATA_ADAPTER.fromBeanList("InitiatorAddresses", "AddressInfo", "sessionID", rows); } catch (OpenDataException e) { throw JmxSupport.toIOException(e); } @@ -53,4 +63,51 @@ public TabularData getEndpoints() throws IOException { public int getQueueSize() { return initiator.getQueueSize(); } + + public static class SessionInitiatorAddressRow { + + private final SessionID sessionID; + private final SocketAddress localInitiatorAddress; + private final SocketAddress[] initiatorAddresses; + private final ObjectName sessionName; + + public SessionInitiatorAddressRow(SessionID sessionID, SocketAddress localInitiatorAddress, + SocketAddress[] initiatorAddresses, ObjectName sessionName) { + this.sessionID = sessionID; + this.localInitiatorAddress = localInitiatorAddress; + this.initiatorAddresses = initiatorAddresses; + this.sessionName = sessionName; + } + + public String getLocalInitiatorAddress() { + if (localInitiatorAddress == null) { + return null; + } else { + InetSocketAddress inetSocketAddress = (InetSocketAddress) localInitiatorAddress; + return inetSocketAddress.getAddress().getHostAddress() + ":" + inetSocketAddress.getPort(); + } + } + + public String getInitiatorAddresses() { + StringBuilder builder = new StringBuilder(128); + + InetSocketAddress inetSocketAddress = (InetSocketAddress) initiatorAddresses[0]; + builder.append(inetSocketAddress.getAddress().getHostAddress()).append(':').append(inetSocketAddress.getPort()); + + for (int i = 1; i < initiatorAddresses.length; i++) { + inetSocketAddress = (InetSocketAddress) initiatorAddresses[i]; + builder.append(',').append(inetSocketAddress.getAddress().getHostAddress()).append(':').append(inetSocketAddress.getPort()); + } + + return builder.toString(); + } + + public SessionID getSessionID() { + return sessionID; + } + + public ObjectName getSessionName() { + return sessionName; + } + } } diff --git a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketInitiatorAdminMBean.java b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketInitiatorAdminMBean.java index 78f1445353..de31395501 100644 --- a/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketInitiatorAdminMBean.java +++ b/quickfixj-core/src/main/java/org/quickfixj/jmx/mbean/connector/SocketInitiatorAdminMBean.java @@ -24,11 +24,8 @@ public interface SocketInitiatorAdminMBean extends ConnectorAdminMBean { /** - * Get initiator communication endpoints - * - * @return a table of endpoint information - * @throws IOException + * @return the initiator addresses configured for this connector's sessions. */ - TabularData getEndpoints() throws IOException; + TabularData getInitiatorAddresses() throws IOException; } 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 e6c3810a00..527325b060 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 @@ -135,7 +135,7 @@ public String getRemoteIPAddress() { /* (non-Javadoc) * @see quickfix.jmx.SessionMBean#reset() */ - public void reset() throws IOException { + public void reset() { logInvocation("reset"); session.reset(); } @@ -391,35 +391,47 @@ public ObjectName preRegister(MBeanServer server, ObjectName name) throws Except // Session State Notifications // - public void onConnect() { + public void onConnect(SessionID sessionID) { sendNotification("connect"); } - public void onDisconnect() { + public void onDisconnect(SessionID sessionID) { sendNotification("disconnect"); } - public void onLogon() { + public void onLogon(SessionID sessionID) { sendNotification("logon"); } - public void onLogout() { + public void onLogout(SessionID sessionID) { sendNotification("logout"); } - public void onHeartBeatTimeout() { + public void onHeartBeatTimeout(SessionID sessionID) { sendNotification("heartBeatTimeout"); } - public void onMissedHeartBeat() { + public void onMissedHeartBeat(SessionID sessionID) { sendNotification("missedHeartBeat"); } - public void onRefresh() { + public void onRefresh(SessionID sessionID) { sendNotification("refresh"); } - public void onReset() { + public void onResendRequestSent(SessionID sessionID, int beginSeqNo, int endSeqNo, int currentEndSeqNo) { + sendNotification("resendRequestSent"); + } + + public void onSequenceResetReceived(SessionID sessionID, int newSeqNo, boolean gapFillFlag) { + sendNotification("sequenceResetReceived"); + } + + public void onResendRequestSatisfied(SessionID sessionID, int beginSeqNo, int endSeqNo) { + sendNotification("resentRequestSatisfied"); + } + + public void onReset(SessionID sessionID) { sendNotification("reset"); } 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 7146528c55..667c3289de 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 @@ -21,31 +21,26 @@ 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 +import java.util.List; public class CompositeTypeFactory { + private final String name; private final String description; - 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 List itemNames = new ArrayList<>(); + private final List itemDescriptions = new ArrayList<>(); + private final List> itemTypes = new ArrayList<>(); public CompositeTypeFactory(String name, String description) { this.name = name; this.description = description; } - @SuppressWarnings("rawtypes") // Java 5/6 incompatibility - public void defineItem(String itemName, OpenType itemType) { - defineItem(itemName, null, itemType); + public void defineItem(String itemName, OpenType itemType) { + defineItem(itemName, itemName, itemType); } - @SuppressWarnings("rawtypes") // Java 5/6 incompatibility - public void defineItem(String itemName, String itemDesc, OpenType itemType) { + public void defineItem(String itemName, String itemDesc, OpenType itemType) { itemNames.add(itemName); itemDescriptions.add(itemDesc); itemTypes.add(itemType); @@ -57,5 +52,4 @@ public CompositeType createCompositeType() throws OpenDataException { .toArray(new String[itemDescriptions.size()]), itemTypes .toArray(new OpenType[itemTypes.size()])); } - } diff --git a/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java b/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java index f5048cffbe..2b7e5faa23 100644 --- a/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java +++ b/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java @@ -66,7 +66,9 @@ public final Product build() throws ConfigError { if (logFactory == null) { logFactory = new ScreenLogFactory(settings); } - + if (messageFactory == null) { + messageFactory = new DefaultMessageFactory(); + } return doBuild(); } diff --git a/quickfixj-core/src/main/java/quickfix/Acceptor.java b/quickfixj-core/src/main/java/quickfix/Acceptor.java index 9a35dd9f21..434c4aa67e 100644 --- a/quickfixj-core/src/main/java/quickfix/Acceptor.java +++ b/quickfixj-core/src/main/java/quickfix/Acceptor.java @@ -16,7 +16,6 @@ * Contact ask@quickfixengine.org if any conditions of this licensing * are not clear to you. ******************************************************************************/ - package quickfix; /** @@ -25,7 +24,8 @@ public interface Acceptor extends Connector { /** - * Acceptor setting specifying the socket protocol used to accept connections. + * Acceptor setting specifying the socket protocol used to accept + * connections. */ String SETTING_SOCKET_ACCEPT_PROTOCOL = "SocketAcceptProtocol"; @@ -35,12 +35,13 @@ public interface Acceptor extends Connector { String SETTING_SOCKET_ACCEPT_PORT = "SocketAcceptPort"; /** - * Acceptor setting specifying local IP interface address for accepting connections. + * Acceptor setting specifying local IP interface address for accepting + * connections. */ String SETTING_SOCKET_ACCEPT_ADDRESS = "SocketAcceptAddress"; /** - * Acceptor setting specifying local IP interface address for accepting connections. + * Acceptor setting specifying a template acceptor session. */ String SETTING_ACCEPTOR_TEMPLATE = "AcceptorTemplate"; } diff --git a/quickfixj-core/src/main/java/quickfix/BoundInMemoryMessageQueue.java b/quickfixj-core/src/main/java/quickfix/BoundInMemoryMessageQueue.java new file mode 100644 index 0000000000..52a3481cb1 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/BoundInMemoryMessageQueue.java @@ -0,0 +1,45 @@ +package quickfix; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A bounded-size version of InMemoryMessageQueue. + * @see InMemoryMessageQueue + */ +public class BoundInMemoryMessageQueue implements MessageQueue { + // The map should be accessed from a single thread + private final Map backingMap = new LinkedHashMap<>(); + private final int maxSize; + + public BoundInMemoryMessageQueue(int maxSize) { + this.maxSize = maxSize; + } + + public void enqueue(int sequence, Message message) { + if (backingMap.size() >= maxSize) { + List keys = backingMap.keySet().stream().sorted().collect(Collectors.toList()); + if (sequence < keys.get(0)) { + backingMap.remove(keys.get(keys.size()-1)); + backingMap.put(sequence, message); + } + } else { + this.backingMap.put(sequence, message); + } + } + + public Message dequeue(int sequence) { + return (Message) this.backingMap.remove(sequence); + } + + public void clear() { + this.backingMap.clear(); + } + + Map getBackingMap() { + return this.backingMap; + } +} + diff --git a/quickfixj-core/src/main/java/quickfix/CachedFileStore.java b/quickfixj-core/src/main/java/quickfix/CachedFileStore.java index 37941ba97f..8464e60d17 100644 --- a/quickfixj-core/src/main/java/quickfix/CachedFileStore.java +++ b/quickfixj-core/src/main/java/quickfix/CachedFileStore.java @@ -28,6 +28,7 @@ import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -86,8 +87,6 @@ public class CachedFileStore implements MessageStore { private FileOutputStream headerFileOutputStream; - private final String charsetEncoding = CharsetSupport.getCharset(); - CachedFileStore(String path, SessionID sessionID, boolean syncWrites) throws IOException { this.syncWrites = syncWrites; @@ -316,16 +315,15 @@ public boolean get(int sequence, String message) { throw new UnsupportedOperationException("not supported"); } - private String read(long offset, long size) throws IOException { - final byte[] data = new byte[(int) size]; - - messageFileReader.seek(offset); - if (messageFileReader.read(data) != size) { - throw new IOException("Truncated input while reading message: " - + new String(data, charsetEncoding)); + private String read(long offset, int size) throws IOException { + try { + final byte[] data = new byte[size]; + messageFileReader.seek(offset); + messageFileReader.readFully(data); + return new String(data, CharsetSupport.getCharset()); + } catch (EOFException eofe) { // can't read fully + throw new IOException("Truncated input while reading message: offset=" + offset + ", expected size=" + size, eofe); } - - return new String(data, charsetEncoding); } private Collection getMessage(long startSequence, long endSequence) throws IOException { @@ -334,7 +332,7 @@ private Collection getMessage(long startSequence, long endSequence) thro final List offsetAndSizes = messageIndex.get(startSequence, endSequence); for (final long[] offsetAndSize : offsetAndSizes) { if (offsetAndSize != null) { - final String message = read(offsetAndSize[0], offsetAndSize[1]); + final String message = read(offsetAndSize[0], (int) offsetAndSize[1]); messages.add(message); } } @@ -349,7 +347,8 @@ private Collection getMessage(long startSequence, long endSequence) thro */ public boolean set(int sequence, String message) throws IOException { final long offset = messageFileWriter.getFilePointer(); - final int size = message.length(); + final byte[] messageBytes = message.getBytes(CharsetSupport.getCharset()); + final int size = messageBytes.length; messageIndex.put((long) sequence, new long[] { offset, size }); headerDataOutputStream.writeInt(sequence); headerDataOutputStream.writeLong(offset); @@ -358,7 +357,7 @@ public boolean set(int sequence, String message) throws IOException { if (syncWrites) { headerFileOutputStream.getFD().sync(); } - messageFileWriter.write(message.getBytes(CharsetSupport.getCharset())); + messageFileWriter.write(messageBytes); return true; } @@ -386,7 +385,7 @@ String getSeqNumFileName() { /* * (non-Javadoc) - * @see quickfix.RefreshableMessageStore#refresh() + * @see quickfix.MessageStore#refresh() */ public void refresh() throws IOException { initialize(false); diff --git a/quickfixj-core/src/main/java/quickfix/DefaultMessageFactory.java b/quickfixj-core/src/main/java/quickfix/DefaultMessageFactory.java index 87fcb08f79..d55440bc0e 100644 --- a/quickfixj-core/src/main/java/quickfix/DefaultMessageFactory.java +++ b/quickfixj-core/src/main/java/quickfix/DefaultMessageFactory.java @@ -35,6 +35,7 @@ import static quickfix.FixVersions.FIX50; import static quickfix.FixVersions.FIX50SP1; import static quickfix.FixVersions.FIX50SP2; +import static quickfix.FixVersions.FIXLATEST; /** * The default factory for creating FIX message instances. @@ -55,7 +56,7 @@ public class DefaultMessageFactory implements MessageFactory { * Equivalent to {@link #DefaultMessageFactory(String) DefaultMessageFactory}({@link ApplVerID#FIX50 ApplVerID.FIX50}). */ public DefaultMessageFactory() { - this(ApplVerID.FIX50SP2); + this(ApplVerID.FIXLATEST); } /** @@ -84,6 +85,7 @@ public DefaultMessageFactory(String defaultApplVerID) { addFactory(FIX50); addFactory(FIX50SP1); addFactory(FIX50SP2); + addFactory(FIXLATEST); } private void addFactory(String beginString) { diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java index 66e3c67c8b..cd83b72206 100644 --- a/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java +++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionFactory.java @@ -25,8 +25,10 @@ import quickfix.field.DefaultApplVerID; import java.net.InetAddress; +import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; +import java.util.List; import java.util.Properties; import java.util.Set; @@ -45,6 +47,7 @@ public class DefaultSessionFactory implements SessionFactory { private final Application application; private final MessageStoreFactory messageStoreFactory; + private final MessageQueueFactory messageQueueFactory; private final LogFactory logFactory; private final MessageFactory messageFactory; private final SessionScheduleFactory sessionScheduleFactory; @@ -53,6 +56,7 @@ public DefaultSessionFactory(Application application, MessageStoreFactory messag LogFactory logFactory) { this.application = application; this.messageStoreFactory = messageStoreFactory; + this.messageQueueFactory = new InMemoryMessageQueueFactory(); this.logFactory = logFactory; this.messageFactory = new DefaultMessageFactory(); this.sessionScheduleFactory = new DefaultSessionScheduleFactory(); @@ -62,6 +66,7 @@ public DefaultSessionFactory(Application application, MessageStoreFactory messag LogFactory logFactory, MessageFactory messageFactory) { this.application = application; this.messageStoreFactory = messageStoreFactory; + this.messageQueueFactory = new InMemoryMessageQueueFactory(); this.logFactory = logFactory; this.messageFactory = messageFactory; this.sessionScheduleFactory = new DefaultSessionScheduleFactory(); @@ -72,6 +77,18 @@ public DefaultSessionFactory(Application application, MessageStoreFactory messag SessionScheduleFactory sessionScheduleFactory) { this.application = application; this.messageStoreFactory = messageStoreFactory; + this.messageQueueFactory = new InMemoryMessageQueueFactory(); + this.logFactory = logFactory; + this.messageFactory = messageFactory; + this.sessionScheduleFactory = sessionScheduleFactory; + } + + public DefaultSessionFactory(Application application, MessageStoreFactory messageStoreFactory, + MessageQueueFactory messageQueueFactory, LogFactory logFactory, + MessageFactory messageFactory, SessionScheduleFactory sessionScheduleFactory) { + this.application = application; + this.messageStoreFactory = messageStoreFactory; + this.messageQueueFactory = messageQueueFactory; this.logFactory = logFactory; this.messageFactory = messageFactory; this.sessionScheduleFactory = sessionScheduleFactory; @@ -157,7 +174,7 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf if (connectionType.equals(SessionFactory.INITIATOR_CONNECTION_TYPE)) { heartbeatInterval = (int) settings.getLong(sessionID, Session.SETTING_HEARTBTINT); if (heartbeatInterval <= 0) { - throw new ConfigError("Heartbeat must be greater than zero"); + throw new ConfigError("Heartbeat interval must be greater than zero"); } } @@ -168,6 +185,9 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf final double testRequestDelayMultiplier = getSetting(settings, sessionID, Session.SETTING_TEST_REQUEST_DELAY_MULTIPLIER, Session.DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER); + final double heartBeatTimeoutMultiplier = getSetting(settings, sessionID, + Session.SETTING_HEARTBEAT_TIMEOUT_MULTIPLIER, + Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER); final UtcTimestampPrecision timestampPrecision = getTimestampPrecision(settings, sessionID, UtcTimestampPrecision.MILLIS); @@ -181,7 +201,7 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf final boolean resetOnLogon = getSetting(settings, sessionID, Session.SETTING_RESET_ON_LOGON, false); - final boolean refreshAtLogon = getSetting(settings, sessionID, + final boolean refreshOnLogon = getSetting(settings, sessionID, Session.SETTING_REFRESH_ON_LOGON, false); final boolean checkCompID = getSetting(settings, sessionID, Session.SETTING_CHECK_COMP_ID, @@ -208,23 +228,26 @@ public Session create(SessionID sessionID, SessionSettings settings) throws Conf final boolean enableNextExpectedMsgSeqNum = getSetting(settings, sessionID, Session.SETTING_ENABLE_NEXT_EXPECTED_MSG_SEQ_NUM, false); final boolean enableLastMsgSeqNumProcessed = getSetting(settings, sessionID, Session.SETTING_ENABLE_LAST_MSG_SEQ_NUM_PROCESSED, false); final int resendRequestChunkSize = getSetting(settings, sessionID, Session.SETTING_RESEND_REQUEST_CHUNK_SIZE, Session.DEFAULT_RESEND_RANGE_CHUNK_SIZE); + final boolean allowPossDup = getSetting(settings, sessionID, Session.SETTING_ALLOW_POS_DUP_MESSAGES, false); final 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, sessionSchedule, logFactory, + final List logonTags = getLogonTags(settings, sessionID); + + final Session session = new Session(application, messageStoreFactory, messageQueueFactory, + sessionID, dataDictionaryProvider, sessionSchedule, logFactory, messageFactory, heartbeatInterval, checkLatency, maxLatency, timestampPrecision, - resetOnLogon, resetOnLogout, resetOnDisconnect, refreshAtLogon, checkCompID, + resetOnLogon, resetOnLogout, resetOnDisconnect, refreshOnLogon, checkCompID, redundantResentRequestAllowed, persistMessages, useClosedIntervalForResend, testRequestDelayMultiplier, senderDefaultApplVerID, validateSequenceNumbers, logonIntervals, resetOnError, disconnectOnError, disableHeartBeatCheck, rejectGarbledMessage, rejectInvalidMessage, rejectMessageOnUnhandledException, requiresOrigSendingTime, forceResendWhenCorruptedStore, allowedRemoteAddresses, validateIncomingMessage, resendRequestChunkSize, enableNextExpectedMsgSeqNum, enableLastMsgSeqNumProcessed, - validateChecksum); + validateChecksum, logonTags, heartBeatTimeoutMultiplier, allowPossDup); session.setLogonTimeout(logonTimeout); session.setLogoutTimeout(logoutTimeout); @@ -427,4 +450,21 @@ private UtcTimestampPrecision getTimestampPrecision(SessionSettings settings, Se } } + private List getLogonTags(SessionSettings settings, SessionID sessionID) throws ConfigError, FieldConvertError { + List logonTags = new ArrayList<>(); + for (int index = 0;; index++) { + final String logonTagSetting = Session.SETTING_LOGON_TAG + + (index == 0 ? "" : NumbersCache.get(index)); + if (settings.isSetting(sessionID, logonTagSetting)) { + String tag = settings.getString(sessionID, logonTagSetting); + String[] split = tag.split("=", 2); + StringField stringField = new StringField(Integer.valueOf(split[0]), split[1]); + logonTags.add(stringField); + } else { + break; + } + } + return logonTags; + } + } diff --git a/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java b/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java index d3b537f71e..48e2f20da3 100644 --- a/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java +++ b/quickfixj-core/src/main/java/quickfix/DefaultSessionSchedule.java @@ -345,7 +345,10 @@ public String toString() { private void formatTimeInterval(StringBuilder buf, TimeInterval timeInterval, SimpleDateFormat timeFormat, boolean local) { - if (isWeekdaySession) { + if (isNonStopSession) { + buf.append("nonstop"); + return; + } else if (isWeekdaySession) { try { for (int i = 0; i < weekdayOffsets.length; i++) { buf.append(DayConverter.toString(weekdayOffsets[i])); diff --git a/quickfixj-core/src/main/java/quickfix/FileStore.java b/quickfixj-core/src/main/java/quickfix/FileStore.java index f9e3068a48..4cf6a5127f 100644 --- a/quickfixj-core/src/main/java/quickfix/FileStore.java +++ b/quickfixj-core/src/main/java/quickfix/FileStore.java @@ -63,7 +63,6 @@ public class FileStore implements MessageStore, Closeable { private final String sessionFileName; private final boolean syncWrites; private final int maxCachedMsgs; - private final String charsetEncoding = CharsetSupport.getCharset(); private RandomAccessFile messageFileReader; private RandomAccessFile messageFileWriter; private DataOutputStream headerDataOutputStream; @@ -358,7 +357,7 @@ private String getMessage(long offset, int size, int i) throws IOException { final byte[] data = new byte[size]; messageFileReader.seek(offset); messageFileReader.readFully(data); - return new String(data, charsetEncoding); + return new String(data, CharsetSupport.getCharset()); } catch (EOFException eofe) { // can't read fully throw new IOException("Truncated input while reading message: messageIndex=" + i + ", offset=" + offset + ", expected size=" + size, eofe); @@ -371,7 +370,8 @@ private String getMessage(long offset, int size, int i) throws IOException { @Override public boolean set(int sequence, String message) throws IOException { final long offset = messageFileWriter.getFilePointer(); - final int size = message.length(); + final byte[] messageBytes = message.getBytes(CharsetSupport.getCharset()); + final int size = messageBytes.length; if (messageIndex != null) { updateMessageIndex(sequence, offset, size); } @@ -382,7 +382,7 @@ public boolean set(int sequence, String message) throws IOException { if (syncWrites) { headerFileOutputStream.getFD().sync(); } - messageFileWriter.write(message.getBytes(CharsetSupport.getCharset())); + messageFileWriter.write(messageBytes); return true; } @@ -398,7 +398,7 @@ private void storeTargetSequenceNumber() throws IOException { /* * (non-Javadoc) - * @see quickfix.RefreshableMessageStore#refresh() + * @see quickfix.MessageStore#refresh() */ @Override public void refresh() throws IOException { diff --git a/quickfixj-core/src/main/java/quickfix/InMemoryMessageQueue.java b/quickfixj-core/src/main/java/quickfix/InMemoryMessageQueue.java new file mode 100644 index 0000000000..9c16dd619b --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/InMemoryMessageQueue.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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.LinkedHashMap; +import java.util.Map; + +/** + * An in-memory implementation of MessageQueue. + * It uses a linked hash map as a backing map. + * + * @see MessageQueue + */ +public class InMemoryMessageQueue implements MessageQueue { + + // The map should be accessed from a single thread + private final Map backingMap = new LinkedHashMap<>(); + + @Override + public void enqueue(int sequence, Message message) { + backingMap.put(sequence, message); + } + + @Override + public Message dequeue(int sequence) { + return backingMap.remove(sequence); + } + + @Override + public void clear() { + backingMap.clear(); + } + + // used in tests + Map getBackingMap() { + return backingMap; + } +} diff --git a/quickfixj-core/src/main/java/quickfix/InMemoryMessageQueueFactory.java b/quickfixj-core/src/main/java/quickfix/InMemoryMessageQueueFactory.java new file mode 100644 index 0000000000..a6b8b4502e --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/InMemoryMessageQueueFactory.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * 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 message queue that stores all messages in memory. + * + * @see MessageQueue + */ +public class InMemoryMessageQueueFactory implements MessageQueueFactory { + + @Override + public MessageQueue create(SessionID sessionID) { + return new InMemoryMessageQueue(); + } +} diff --git a/quickfixj-core/src/main/java/quickfix/JdbcLog.java b/quickfixj-core/src/main/java/quickfix/JdbcLog.java index 308e37fade..51f7d124af 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcLog.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcLog.java @@ -55,7 +55,7 @@ class JdbcLog extends AbstractLog { private final Map deleteItemsSqlCache = new HashMap<>(); public JdbcLog(SessionSettings settings, SessionID sessionID, DataSource ds) - throws SQLException, ClassNotFoundException, ConfigError, FieldConvertError { + throws SQLException, ConfigError, FieldConvertError { this.sessionID = sessionID; dataSource = ds == null ? JdbcUtil.getDataSource(settings, sessionID) @@ -109,8 +109,8 @@ private void createCachedSql() { } private void createInsertItemSql(String tableName) { - insertItemSqlCache.put(tableName, "INSERT INTO " + tableName + " (time, " - + getIDColumns(extendedSessionIdSupported) + ", text) " + "VALUES (?," + insertItemSqlCache.put(tableName, "INSERT INTO " + tableName + " (time," + + getIDColumns(extendedSessionIdSupported) + ",text) " + "VALUES (?," + getIDPlaceholders(extendedSessionIdSupported) + ",?)"); } diff --git a/quickfixj-core/src/main/java/quickfix/JdbcSetting.java b/quickfixj-core/src/main/java/quickfix/JdbcSetting.java index 59c31d68e9..7653ee7f59 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcSetting.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcSetting.java @@ -19,6 +19,8 @@ package quickfix; +import javax.sql.DataSource; + /** * Class for storing JDBC setting constants shared by both the log and message * store classes. @@ -103,40 +105,62 @@ public class JdbcSetting { public static final String SETTING_JDBC_SESSION_ID_DEFAULT_PROPERTY_VALUE = "JdbcSessionIdDefaultPropertyValue"; /** - * Specifies the maximum number of connections to the database - * - * @see http://proxool.sourceforge.net/properties.html + * Controls the maximum size that the pool is allowed to reach, including both idle and in-use connections. + * Basically this value will determine the maximum number of actual connections to the database backend. + * A reasonable value for this is best determined by your execution environment. When the pool reaches this size, + * and no idle connections are available, calls to {@link DataSource#getConnection()} will block for up to + * {@link JdbcSetting#SETTING_JDBC_CONNECTION_TIMEOUT} milliseconds before timing out. */ public static final String SETTING_JDBC_MAX_ACTIVE_CONNECTION = "JdbcMaxActiveConnection"; /** - * Specifies if the housekeeper comes across a thread that has been active for longer than - * this then it will kill it. So make sure you set this to a number bigger than your - * slowest expected response! - * - * @see http://proxool.sourceforge.net/properties.html + * Controls the minimum number of idle connections that HikariCP tries to maintain in the pool. + * If the idle connections dip below this value and total connections in the pool are less than + * {@link JdbcSetting#SETTING_JDBC_MAX_ACTIVE_CONNECTION}, HikariCP will make the best effort to add + * additional connections quickly and efficiently. However, for maximum performance and responsiveness + * to spike demands, we recommend not setting this value and instead allowing HikariCP to act as a fixed + * size connection pool. */ - public static final String SETTING_JDBC_MAX_ACTIVE_TIME = "JdbcMaxActiveTime"; + public static final String SETTING_JDBC_MIN_IDLE_CONNECTION = "JdbcMinIdleConnection"; /** - * Specifies the maximum amount of time that a connection exists for before - * it is killed (milliseconds). - * - * @see http://proxool.sourceforge.net/properties.html + * Controls the maximum lifetime of a connection in the pool. An in-use connection will never be retired, only when + * it is closed will it then be removed. On a connection-by-connection basis, minor negative attenuation is applied to + * avoid mass-extinction in the pool. We strongly recommend setting this value, and it should be several seconds shorter + * than any database or infrastructure imposed connection time limit. A value of 0 indicates no maximum lifetime (infinite + * lifetime), subject of course to the {@link JdbcSetting#SETTING_JDBC_CONNECTION_IDLE_TIMEOUT} setting. */ public static final String SETTING_JDBC_MAX_CONNECTION_LIFETIME = "JdbcMaxConnectionLifeTime"; /** - * Specifies the maximum number of connections we can be building at any one time. - * That is, the number of new connections that have been requested but aren't yet - * available for use. Because connections can be built using more than one thread - * (for instance, when they are built on demand) and it takes a finite time between - * deciding to build the connection and it becoming available we need some way of - * ensuring that a lot of threads don't all decide to build a connection at once. - * (We could solve this in a smarter way - and indeed we will one day) - * - * @see http://proxool.sourceforge.net/properties.html + * Controls the maximum number of milliseconds that a client (that's you) will wait for a connection from the pool. If this time + * is exceeded without a connection becoming available, an SQLException will be thrown. Lowest acceptable connection timeout is 250 ms. */ - public static final String SETTING_JDBC_SIMULTANEOUS_BUILD_THROTTLE = "JdbcSimultaneousBuildThrottle"; + public static final String SETTING_JDBC_CONNECTION_TIMEOUT = "JdbcConnectionTimeout"; + /** + * Controls the maximum amount of time that a connection is allowed to sit idle in the pool. + * This setting only applies when {@link JdbcSetting#SETTING_JDBC_MIN_IDLE_CONNECTION} is defined to be less than + * {@link JdbcSetting#SETTING_JDBC_MAX_ACTIVE_CONNECTION}. Idle connections will not be retired once the pool + * reaches {@link JdbcSetting#SETTING_JDBC_MIN_IDLE_CONNECTION} connections. + */ + public static final String SETTING_JDBC_CONNECTION_IDLE_TIMEOUT = "JdbcConnectionIdleTimeout"; + + /** + * Controls how frequently HikariCP will attempt to keep a connection alive, in order to prevent it from being timed out by the + * database or network infrastructure. This value must be less than the {@link JdbcSetting#SETTING_JDBC_MAX_CONNECTION_LIFETIME} value. + * A "keepalive" will only occur on an idle connection. When the time arrives for a "keepalive" against a given connection, that connection + * will be removed from the pool, "pinged", and then returned to the pool. The 'ping' is one of either: invocation of the JDBC4 + * {@link java.sql.Connection#isValid(int)} method, or execution of the {@link JdbcSetting#SETTING_JDBC_CONNECTION_TEST_QUERY}. Typically, the duration + * out-of-the-pool should be measured in single digit milliseconds or even sub-millisecond, and therefore should have little or no noticeable + * performance impact. The minimum allowed value is 30000ms (30 seconds), but a value in the range of minutes is most desirable. + */ + public static final String SETTING_JDBC_CONNECTION_KEEPALIVE_TIME = "JdbcConnectionKeepaliveTime"; + + /** + * If your driver supports JDBC4 we strongly recommend not setting this property. This is for "legacy" drivers that do not support the + * JDBC4 {@link java.sql.Connection#isValid(int)} API. This is the query that will be executed just before a connection is given to you + * from the pool to validate that the connection to the database is still alive. + */ + public static final String SETTING_JDBC_CONNECTION_TEST_QUERY = "JdbcConnectionTestQuery"; } diff --git a/quickfixj-core/src/main/java/quickfix/JdbcStore.java b/quickfixj-core/src/main/java/quickfix/JdbcStore.java index 235784e635..a1f901e4ed 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcStore.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcStore.java @@ -41,6 +41,7 @@ class JdbcStore implements MessageStore { private final String sessionTableName; private final String messageTableName; private final String defaultSessionIdPropertyValue; + private final boolean persistMessages; private String SQL_UPDATE_SEQNUMS; private String SQL_INSERT_SESSION; @@ -74,6 +75,9 @@ public JdbcStore(SessionSettings settings, SessionID sessionID, DataSource ds) t defaultSessionIdPropertyValue = SessionID.NOT_SET; } + persistMessages = !settings.isSetting(sessionID, Session.SETTING_PERSIST_MESSAGES) || + settings.getBool(sessionID, Session.SETTING_PERSIST_MESSAGES); + dataSource = ds == null ? JdbcUtil.getDataSource(settings, sessionID) : ds; // One table is sampled for the extended session ID columns. Be sure @@ -184,9 +188,11 @@ public void reset() throws IOException { PreparedStatement updateTime = null; try { connection = dataSource.getConnection(); - deleteMessages = connection.prepareStatement(SQL_DELETE_MESSAGES); - setSessionIdParameters(deleteMessages, 1); - deleteMessages.execute(); + if (persistMessages) { + deleteMessages = connection.prepareStatement(SQL_DELETE_MESSAGES); + setSessionIdParameters(deleteMessages, 1); + deleteMessages.execute(); + } updateTime = connection.prepareStatement(SQL_UPDATE_SESSION); updateTime.setTimestamp(1, new Timestamp(Calendar.getInstance( diff --git a/quickfixj-core/src/main/java/quickfix/JdbcUtil.java b/quickfixj-core/src/main/java/quickfix/JdbcUtil.java index 14ffc259e1..c6ce2c2fda 100644 --- a/quickfixj-core/src/main/java/quickfix/JdbcUtil.java +++ b/quickfixj-core/src/main/java/quickfix/JdbcUtil.java @@ -19,7 +19,9 @@ package quickfix; -import org.logicalcobwebs.proxool.ProxoolDataSource; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import javax.naming.InitialContext; import javax.naming.NamingException; @@ -31,89 +33,96 @@ import java.sql.SQLException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; class JdbcUtil { static final String CONNECTION_POOL_ALIAS = "quickfixj"; + static final int DEFAULT_MAX_CONNECTION_COUNT = 32; + static final long DEFAULT_MAX_CONNECTION_LIFETIME = TimeUnit.HOURS.toMillis(8); + static final long DEFAULT_CONNECTION_TIMEOUT = 250L; + static final long DEFAULT_CONNECTION_IDLE_TIMEOUT = TimeUnit.MINUTES.toMillis(10); + static final long DEFAULT_CONNECTION_KEEPALIVE_TIME = 0; - private static final Map dataSources = new ConcurrentHashMap<>(); + private static final Map dataSources = new ConcurrentHashMap<>(); private static final AtomicInteger dataSourceCounter = new AtomicInteger(); - static DataSource getDataSource(SessionSettings settings, SessionID sessionID) - throws ConfigError, FieldConvertError { + static DataSource getDataSource(SessionSettings settings, SessionID sessionID) throws ConfigError, FieldConvertError { if (settings.isSetting(sessionID, JdbcSetting.SETTING_JDBC_DS_NAME)) { - String jndiName = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_DS_NAME); - try { - return (DataSource) new InitialContext().lookup(jndiName); - } catch (NamingException e) { - throw new ConfigError(e); - } + return getJNDIDataSource(settings, sessionID); } else { - String jdbcDriver = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_DRIVER); - String connectionURL = settings.getString(sessionID, - JdbcSetting.SETTING_JDBC_CONNECTION_URL); - String user = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_USER); - String password = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_PASSWORD); - int maxConnCount = settings - .isSetting(JdbcSetting.SETTING_JDBC_MAX_ACTIVE_CONNECTION) ? - settings.getInt(sessionID, JdbcSetting.SETTING_JDBC_MAX_ACTIVE_CONNECTION) : - 32; - int simultaneousBuildThrottle = settings - .isSetting(JdbcSetting.SETTING_JDBC_SIMULTANEOUS_BUILD_THROTTLE) ? - settings.getInt(sessionID, JdbcSetting.SETTING_JDBC_SIMULTANEOUS_BUILD_THROTTLE) : - maxConnCount; - long maxActiveTime = settings - .isSetting(JdbcSetting.SETTING_JDBC_MAX_ACTIVE_TIME) ? - settings.getLong(sessionID, JdbcSetting.SETTING_JDBC_MAX_ACTIVE_TIME) : - 5000; - int maxConnLifetime = settings - .isSetting(JdbcSetting.SETTING_JDBC_MAX_CONNECTION_LIFETIME) ? - settings.getInt(sessionID, JdbcSetting.SETTING_JDBC_MAX_CONNECTION_LIFETIME) : - 28800000; - - return getDataSource(jdbcDriver, connectionURL, user, password, true, maxConnCount, - simultaneousBuildThrottle, maxActiveTime, maxConnLifetime); + return getOrCreatePooledDataSource(settings, sessionID); + } + } + + private static DataSource getJNDIDataSource(SessionSettings settings, SessionID sessionID) throws ConfigError { + String jndiName = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_DS_NAME); + try { + return (DataSource) new InitialContext().lookup(jndiName); + } catch (NamingException e) { + throw new ConfigError(e); } } - static DataSource getDataSource(String jdbcDriver, String connectionURL, String user, String password, boolean cache) { - return getDataSource(jdbcDriver, connectionURL, user, password, cache, 10, 10, 5000, 28800000); + private static DataSource getOrCreatePooledDataSource(SessionSettings settings, SessionID sessionID) throws ConfigError, FieldConvertError { + String jdbcDriver = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_DRIVER); + String connectionURL = settings.getString(sessionID,JdbcSetting.SETTING_JDBC_CONNECTION_URL); + String user = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_USER); + String password = settings.getString(sessionID, JdbcSetting.SETTING_JDBC_PASSWORD); + return getOrCreatePooledDataSource(settings, sessionID, jdbcDriver, connectionURL, user, password); } - /** - * 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 DataSource getDataSource(String jdbcDriver, String connectionURL, String user, String password, - boolean cache, int maxConnCount, int simultaneousBuildThrottle, - long maxActiveTime, int maxConnLifetime) { + static DataSource getOrCreatePooledDataSource(SessionSettings settings, SessionID sessionID, String jdbcDriver, String connectionURL, String user, String password) + throws ConfigError, FieldConvertError { String key = jdbcDriver + "#" + connectionURL + "#" + user + "#" + password; - ProxoolDataSource ds = cache ? dataSources.get(key) : null; - - if (ds == null) { - 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); + + HikariDataSource dataSource = dataSources.get(key); + + if (dataSource != null) { + return dataSource; + } + + HikariDataSource newDataSource = createPooledDataSource(settings, sessionID, jdbcDriver, connectionURL, user, password); + + if (dataSources.putIfAbsent(key, newDataSource) == null) { + return newDataSource; + } else { + return dataSources.get(key); } - return ds; + } + + private static HikariDataSource createPooledDataSource(SessionSettings settings, SessionID sessionID, String jdbcDriver, String connectionURL, String user, String password) + throws ConfigError, FieldConvertError { + HikariConfig configuration = new HikariConfig(); + configuration.setPoolName(CONNECTION_POOL_ALIAS + "-" + dataSourceCounter.incrementAndGet()); + configuration.setDriverClassName(jdbcDriver); + configuration.setJdbcUrl(connectionURL); + configuration.setUsername(user); + configuration.setPassword(password); + + int maxConnectionCount = settings.getIntOrDefault(sessionID, JdbcSetting.SETTING_JDBC_MAX_ACTIVE_CONNECTION, DEFAULT_MAX_CONNECTION_COUNT); + configuration.setMaximumPoolSize(maxConnectionCount); + + int minIdleConnectionCount = settings.getIntOrDefault(sessionID, JdbcSetting.SETTING_JDBC_MIN_IDLE_CONNECTION, maxConnectionCount); + configuration.setMinimumIdle(minIdleConnectionCount); + + long maxConnectionLifetime = settings.getLongOrDefault(sessionID, JdbcSetting.SETTING_JDBC_MAX_CONNECTION_LIFETIME, DEFAULT_MAX_CONNECTION_LIFETIME); + configuration.setMaxLifetime(maxConnectionLifetime); + + long connectionTimeout = settings.getLongOrDefault(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT); + configuration.setConnectionTimeout(connectionTimeout); + + long connectionIdleTimeout = settings.getLongOrDefault(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_IDLE_TIMEOUT, DEFAULT_CONNECTION_IDLE_TIMEOUT); + configuration.setIdleTimeout(connectionIdleTimeout); + + long connectionKeepaliveTime = settings.getLongOrDefault(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_KEEPALIVE_TIME, DEFAULT_CONNECTION_KEEPALIVE_TIME); + configuration.setKeepaliveTime(connectionKeepaliveTime); + + String connectionTestQuery = settings.getStringOrDefault(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_TEST_QUERY, null); + configuration.setConnectionTestQuery(connectionTestQuery); + + return new HikariDataSource(configuration); } static void close(SessionID sessionID, Connection connection) { @@ -165,7 +174,7 @@ private static boolean isColumn(DatabaseMetaData metaData, String tableName, Str static String getIDWhereClause(boolean isExtendedSessionID) { return isExtendedSessionID ? ("beginstring=? and sendercompid=? and sendersubid=? and senderlocid=? and " - + "targetcompid=? and targetsubid=? and targetlocid=? and session_qualifier=? ") + + "targetcompid=? and targetsubid=? and targetlocid=? and session_qualifier=? ") : "beginstring=? and sendercompid=? and targetcompid=? and session_qualifier=? "; } @@ -201,5 +210,4 @@ static int setSessionIdParameters(SessionID sessionID, PreparedStatement query, private static String getSqlValue(String javaValue, String defaultSqlValue) { return !SessionID.NOT_SET.equals(javaValue) ? javaValue : defaultSqlValue; } - } diff --git a/quickfixj-core/src/main/java/quickfix/MessageQueue.java b/quickfixj-core/src/main/java/quickfix/MessageQueue.java new file mode 100644 index 0000000000..8f48cc7624 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/MessageQueue.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * 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; + +/** + * Used by a Session to store and retrieve messages with a sequence number higher than expected. + * + * @see quickfix.Session + */ +public interface MessageQueue { + + /** + * Enqueue a message. + * + * @param sequence the sequence number + * @param message the FIX message + */ + void enqueue(int sequence, Message message); + + /** + * Dequeue a message with given sequence number. + * + * @param sequence the sequence number + * @return message the FIX message + */ + Message dequeue(int sequence); + + /** + * Remove messages from queue up to a given sequence number. + * + * @param seqnum up to which sequence number messages should be deleted + */ + default void dequeueMessagesUpTo(int seqnum) { + for (int i = 1; i < seqnum; i++) { + dequeue(i); + } + } + + /** + * Clear the queue. + */ + void clear(); +} diff --git a/quickfixj-core/src/main/java/quickfix/MessageQueueFactory.java b/quickfixj-core/src/main/java/quickfix/MessageQueueFactory.java new file mode 100644 index 0000000000..0660844024 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/MessageQueueFactory.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * 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; + +/** + * Used by a Session to create a message queue implementation. + * + * @see Session + */ +public interface MessageQueueFactory { + + /** + * Creates a message queue implementation. + * + * @param sessionID the session ID, often used to access session configurations + * @return the message queue implementation + */ + MessageQueue create(SessionID sessionID); +} diff --git a/quickfixj-core/src/main/java/quickfix/MessageSessionUtils.java b/quickfixj-core/src/main/java/quickfix/MessageSessionUtils.java new file mode 100644 index 0000000000..a6eec9e3a6 --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/MessageSessionUtils.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * 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 quickfix.field.ApplVerID; +import quickfix.field.BeginString; +import quickfix.field.DefaultApplVerID; + +public class MessageSessionUtils { + + /** + * NOTE: This method is intended for internal use. + * + * @param session the Session that will process the message + * @param messageString + * @return the parsed message + * @throws InvalidMessage + */ + public static Message parse(Session session, String messageString) throws InvalidMessage { + final String beginString = MessageUtils.getStringField(messageString, BeginString.FIELD); + final String msgType = MessageUtils.getMessageType(messageString); + final MessageFactory messageFactory = session.getMessageFactory(); + final DataDictionaryProvider ddProvider = session.getDataDictionaryProvider(); + final ApplVerID applVerID; + final DataDictionary sessionDataDictionary = ddProvider == null ? null : ddProvider + .getSessionDataDictionary(beginString); + final quickfix.Message message; + final DataDictionary payloadDictionary; + + if (!MessageUtils.isAdminMessage(msgType) || MessageUtils.isLogon(messageString)) { + if (FixVersions.BEGINSTRING_FIXT11.equals(beginString)) { + applVerID = getApplVerID(session, messageString); + } else { + applVerID = MessageUtils.toApplVerID(beginString); + } + final DataDictionary applicationDataDictionary = ddProvider == null ? null : ddProvider + .getApplicationDataDictionary(applVerID); + payloadDictionary = MessageUtils.isAdminMessage(msgType) + ? sessionDataDictionary + : applicationDataDictionary; + } else { + applVerID = null; + payloadDictionary = sessionDataDictionary; + } + + final boolean doValidation = payloadDictionary != null; + final boolean validateChecksum = session.isValidateChecksum(); + + message = messageFactory.create(beginString, applVerID, msgType); + message.parse(messageString, sessionDataDictionary, payloadDictionary, doValidation, + validateChecksum); + + return message; + } + + private static ApplVerID getApplVerID(Session session, String messageString) + throws InvalidMessage { + ApplVerID applVerID = null; + + final String applVerIdString = MessageUtils.getStringField(messageString, ApplVerID.FIELD); + if (applVerIdString != null) { + applVerID = new ApplVerID(applVerIdString); + } + + if (applVerID == null) { + applVerID = session.getTargetDefaultApplicationVersionID(); + } + + if (applVerID == null && MessageUtils.isLogon(messageString)) { + final String defaultApplVerIdString = MessageUtils.getStringField(messageString, + DefaultApplVerID.FIELD); + if (defaultApplVerIdString != null) { + applVerID = new ApplVerID(defaultApplVerIdString); + } + } + + if (applVerID == null) { + throw MessageUtils.newInvalidMessageException("Can't determine ApplVerID from message " + messageString, MessageUtils.getMinimalMessage(messageString)); + } + + return applVerID; + } +} diff --git a/quickfixj-core/src/main/java/quickfix/MessageStore.java b/quickfixj-core/src/main/java/quickfix/MessageStore.java index aaa61ed43a..756c72dad0 100644 --- a/quickfixj-core/src/main/java/quickfix/MessageStore.java +++ b/quickfixj-core/src/main/java/quickfix/MessageStore.java @@ -32,7 +32,7 @@ public interface MessageStore { /** - * Adds a raw fix messages to the store with the given sequence number. + * Adds a raw fix message to the store with the given sequence number. * (Most implementations just append the message data to the store so be * careful about assuming random access behavior.) * diff --git a/quickfixj-core/src/main/java/quickfix/Session.java b/quickfixj-core/src/main/java/quickfix/Session.java index 8fa06c3379..11895383ff 100644 --- a/quickfixj-core/src/main/java/quickfix/Session.java +++ b/quickfixj-core/src/main/java/quickfix/Session.java @@ -113,10 +113,15 @@ public class Session implements Closeable { public static final String SETTING_MAX_LATENCY = "MaxLatency"; /** - * Session setting for the test delay multiplier (0-1, as fraction of Heartbeat interval) + * Session setting for the test delay multiplier (as fraction of heartbeat interval). */ public static final String SETTING_TEST_REQUEST_DELAY_MULTIPLIER = "TestRequestDelayMultiplier"; + /** + * Session setting for the heartbeat timeout multiplier (as fraction of heartbeat interval). + */ + public static final String SETTING_HEARTBEAT_TIMEOUT_MULTIPLIER = "HeartBeatTimeoutMultiplier"; + /** * Session scheduling setting to specify that session never reset */ @@ -224,6 +229,12 @@ public class Session implements Closeable { */ public static final String SETTING_LOGOUT_TIMEOUT = "LogoutTimeout"; + /** + * Session setting for custom logon tags. Single entry or consecutive list of + * tag=value pairs, e.g. LogonTag=553=user and LogonTag1=554=password. + */ + public static final String SETTING_LOGON_TAG = "LogonTag"; + /** * Session setting for doing an automatic sequence number reset on logout. * Valid values are "Y" or "N". Default is "N". @@ -259,9 +270,9 @@ public class Session implements Closeable { public static final String SETTING_DISCONNECT_ON_ERROR = "DisconnectOnError"; /** - * Session setting to control precision in message timestamps. + * 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. + * Only valid for FIX version ">"= 4.2. */ public static final String SETTING_TIMESTAMP_PRECISION = "TimeStampPrecision"; @@ -272,7 +283,7 @@ public class Session implements Closeable { /** * Session setting that causes the session to reset sequence numbers when initiating - * a logon (>= FIX 4.2). + * a logon (">"= FIX 4.2). */ public static final String SETTING_RESET_ON_LOGON = "ResetOnLogon"; @@ -283,7 +294,7 @@ public class Session implements Closeable { /** * Requests that state and message data be refreshed from the message store at - * logon, if possible. This supports simple failover behavior for acceptors + * logon, if possible. This supports simple failover behavior for acceptors. */ public static final String SETTING_REFRESH_ON_LOGON = "RefreshOnLogon"; @@ -306,7 +317,7 @@ public class Session implements Closeable { public static final String SETTING_USE_CLOSED_RESEND_INTERVAL = "ClosedResendInterval"; /** - * Allow unknown fields in messages. This is intended for unknown fields with tags < 5000 + * Allow unknown fields in messages. This is intended for unknown fields with tags "<" 5000 * (not user defined fields) */ public static final String SETTING_ALLOW_UNKNOWN_MSG_FIELDS = "AllowUnknownMsgFields"; @@ -352,6 +363,13 @@ public class Session implements Closeable { public static final String SETTING_ALLOWED_REMOTE_ADDRESSES = "AllowedRemoteAddresses"; + /** + * Log the entire message when the corresponding session can not be found. + * Otherwise only the SessionID is logged. + * Valid values are "Y" or "N". Default is "Y". + */ + public static final String SETTING_LOG_MESSAGE_WHEN_SESSION_NOT_FOUND = "LogMessageWhenSessionNotFound"; + /** * Setting to limit the size of a resend request in case of missing messages. * This is useful when the remote FIX engine does not allow to ask for more than n message for a ResendRequest @@ -362,6 +380,11 @@ public class Session implements Closeable { public static final String SETTING_VALIDATE_CHECKSUM = "ValidateChecksum"; + /** + * Option so that the session does not remove PossDupFlag (43) and OrigSendingTime (122) information when sending. + */ + public static final String SETTING_ALLOW_POS_DUP_MESSAGES = "AllowPosDup"; + private static final ConcurrentMap sessions = new ConcurrentHashMap<>(); private final Application application; @@ -372,7 +395,11 @@ public class Session implements Closeable { // @GuardedBy(this) private final SessionState state; - private boolean enabled; + /* + * Controls whether it is possible to log on to this Session (if Acceptor) + * or if Logon is sent out respectively (if Initiator). + */ + private volatile boolean enabled; private final Object responderLock = new Object(); // unique instance // @GuardedBy(responderLock) @@ -394,7 +421,7 @@ public class Session implements Closeable { private final boolean resetOnError; private final boolean disconnectOnError; private final UtcTimestampPrecision timestampPrecision; - private final boolean refreshMessageStoreAtLogon; + private final boolean refreshOnLogon; private final boolean redundantResentRequestsAllowed; private final boolean persistMessages; private final boolean checkCompID; @@ -408,6 +435,7 @@ public class Session implements Closeable { private boolean enableNextExpectedMsgSeqNum = false; private boolean enableLastMsgSeqNumProcessed = false; private boolean validateChecksum = true; + private boolean allowPosDup = false; private int maxScheduledWriteRequests = 0; @@ -420,14 +448,15 @@ public class Session implements Closeable { private final AtomicReference targetDefaultApplVerID = new AtomicReference<>(); private final DefaultApplVerID senderDefaultApplVerID; - private boolean validateSequenceNumbers = true; - private boolean validateIncomingMessage = true; + private final boolean validateSequenceNumbers; + private final boolean validateIncomingMessage; 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; + public static final double DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER = 1.4; private static final String ENCOUNTERED_END_OF_STREAM = "Encountered END_OF_STREAM"; @@ -437,17 +466,18 @@ public class Session implements Closeable { 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); + private final List logonTags; + 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, UtcTimestampPrecision.MILLIS, - false, false, false, false, true, false, true, false, - DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, null, true, new int[] { 5 }, false, false, - false, false, true, false, true, false, null, true, DEFAULT_RESEND_RANGE_CHUNK_SIZE, false, false, false); + DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule, LogFactory logFactory, + MessageFactory messageFactory, int heartbeatInterval) { + this(application, messageStoreFactory, sessionID, dataDictionaryProvider, sessionSchedule, logFactory, + messageFactory, heartbeatInterval, true, DEFAULT_MAX_LATENCY, UtcTimestampPrecision.MILLIS, false, false, + false, false, true, false, true, false, DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, null, true, new int[] {5}, + false, false, false, false, true, false, true, false, null, true, DEFAULT_RESEND_RANGE_CHUNK_SIZE, false, + false, false, new ArrayList(), DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false); } Session(Application application, MessageStoreFactory messageStoreFactory, SessionID sessionID, @@ -455,7 +485,7 @@ public class Session implements Closeable { LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval, boolean checkLatency, int maxLatency, UtcTimestampPrecision timestampPrecision, boolean resetOnLogon, boolean resetOnLogout, boolean resetOnDisconnect, - boolean refreshMessageStoreAtLogon, boolean checkCompID, + boolean refreshOnLogon, boolean checkCompID, boolean redundantResentRequestsAllowed, boolean persistMessages, boolean useClosedRangeForResend, double testRequestDelayMultiplier, DefaultApplVerID senderDefaultApplVerID, boolean validateSequenceNumbers, @@ -465,7 +495,32 @@ public class Session implements Closeable { boolean forceResendWhenCorruptedStore, Set allowedRemoteAddresses, boolean validateIncomingMessage, int resendRequestChunkSize, boolean enableNextExpectedMsgSeqNum, boolean enableLastMsgSeqNumProcessed, - boolean validateChecksum) { + boolean validateChecksum, List logonTags, double heartBeatTimeoutMultiplier, + boolean allowPossDup) { + this(application, messageStoreFactory, new InMemoryMessageQueueFactory(), sessionID, dataDictionaryProvider, sessionSchedule, logFactory, + messageFactory, heartbeatInterval, true, DEFAULT_MAX_LATENCY, UtcTimestampPrecision.MILLIS, false, false, + false, false, true, false, true, false, DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, null, true, new int[] {5}, + false, false, false, false, true, false, true, false, null, true, DEFAULT_RESEND_RANGE_CHUNK_SIZE, false, + false, false, new ArrayList(), DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, allowPossDup); + } + + Session(Application application, MessageStoreFactory messageStoreFactory, MessageQueueFactory messageQueueFactory, + SessionID sessionID, DataDictionaryProvider dataDictionaryProvider, SessionSchedule sessionSchedule, + LogFactory logFactory, MessageFactory messageFactory, int heartbeatInterval, + boolean checkLatency, int maxLatency, UtcTimestampPrecision timestampPrecision, + boolean resetOnLogon, boolean resetOnLogout, boolean resetOnDisconnect, + boolean refreshOnLogon, boolean checkCompID, + boolean redundantResentRequestsAllowed, boolean persistMessages, + boolean useClosedRangeForResend, double testRequestDelayMultiplier, + DefaultApplVerID senderDefaultApplVerID, boolean validateSequenceNumbers, + int[] logonIntervals, boolean resetOnError, boolean disconnectOnError, + boolean disableHeartBeatCheck, boolean rejectGarbledMessage, boolean rejectInvalidMessage, + boolean rejectMessageOnUnhandledException, boolean requiresOrigSendingTime, + boolean forceResendWhenCorruptedStore, Set allowedRemoteAddresses, + boolean validateIncomingMessage, int resendRequestChunkSize, + boolean enableNextExpectedMsgSeqNum, boolean enableLastMsgSeqNumProcessed, + boolean validateChecksum, List logonTags, double heartBeatTimeoutMultiplier, + boolean allowPossDup) { this.application = application; this.sessionID = sessionID; this.sessionSchedule = sessionSchedule; @@ -475,7 +530,7 @@ public class Session implements Closeable { this.resetOnLogout = resetOnLogout; this.resetOnDisconnect = resetOnDisconnect; this.timestampPrecision = timestampPrecision; - this.refreshMessageStoreAtLogon = refreshMessageStoreAtLogon; + this.refreshOnLogon = refreshOnLogon; this.dataDictionaryProvider = dataDictionaryProvider; this.messageFactory = messageFactory; this.checkCompID = checkCompID; @@ -499,6 +554,8 @@ public class Session implements Closeable { this.enableNextExpectedMsgSeqNum = enableNextExpectedMsgSeqNum; this.enableLastMsgSeqNumProcessed = enableLastMsgSeqNumProcessed; this.validateChecksum = validateChecksum; + this.logonTags = logonTags; + this.allowPosDup = allowPossDup; final Log engineLog = (logFactory != null) ? logFactory.create(sessionID) : null; if (engineLog instanceof SessionStateListener) { @@ -510,8 +567,13 @@ public class Session implements Closeable { addStateListener((SessionStateListener) messageStore); } + final MessageQueue messageQueue = messageQueueFactory.create(sessionID); + if (messageQueue instanceof SessionStateListener) { + addStateListener((SessionStateListener) messageQueue); + } + state = new SessionState(this, engineLog, heartbeatInterval, heartbeatInterval != 0, - messageStore, testRequestDelayMultiplier); + messageStore, messageQueue, testRequestDelayMultiplier, heartBeatTimeoutMultiplier); registerSession(this); @@ -546,9 +608,9 @@ public void setResponder(Responder responder) { synchronized (responderLock) { this.responder = responder; if (responder != null) { - stateListener.onConnect(); + stateListener.onConnect(sessionID); } else { - stateListener.onDisconnect(); + stateListener.onDisconnect(sessionID); } } } @@ -729,7 +791,7 @@ public void logon() { setEnabled(true); } - private synchronized void setEnabled(boolean enabled) { + private void setEnabled(boolean enabled) { this.enabled = enabled; } @@ -786,7 +848,7 @@ public void logout(String reason) { * * @return true if session is enabled, false otherwise. */ - public synchronized boolean isEnabled() { + public boolean isEnabled() { return enabled; } @@ -849,12 +911,11 @@ private boolean isResetNeeded() { } /** - * Logs out and disconnects session (if logged on) and then resets session state. + * Logs out session (if logged on) and then resets session state. * - * @throws IOException IO error * @see SessionState#reset() */ - public void reset() throws IOException { + public void reset() { if (!isResetting.compareAndSet(false, true)) { return; } @@ -863,10 +924,12 @@ public void reset() throws IOException { if (application instanceof ApplicationExtended) { ((ApplicationExtended) application).onBeforeSessionReset(sessionID); } - generateLogout(); - disconnect("Session reset", false); + state.setResetStatePending(true); + generateLogout("Session reset"); + getLog().onEvent("Initiated logout request"); + } else { + resetState(); } - resetState(); } finally { isResetting.set(false); } @@ -1221,12 +1284,8 @@ private boolean resetOrDisconnectIfRequired(Message msg) { return false; } if (resetOnError) { - try { - getLog().onErrorEvent("Auto reset"); - reset(); - } catch (final IOException e) { - LOG.error("Failed resetting: {}", e); - } + getLog().onErrorEvent("Auto reset"); + reset(); return true; } if (disconnectOnError) { @@ -1240,10 +1299,6 @@ private boolean resetOrDisconnectIfRequired(Message msg) { return false; } - private boolean isStateRefreshNeeded(String msgType) { - return refreshMessageStoreAtLogon && !state.isInitiator() && MsgType.LOGON.equals(msgType); - } - private void nextReject(Message reject) throws FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException, InvalidMessage { if (!verify(reject, false, validateSequenceNumbers)) { @@ -1324,7 +1379,7 @@ private String formatEndSeqNum(int seqNo) { } private Message parseMessage(String messageData) throws InvalidMessage { - return MessageUtils.parse(this, messageData); + return MessageSessionUtils.parse(this, messageData); } private boolean isTargetTooLow(int msgSeqNum) throws IOException { @@ -1393,6 +1448,7 @@ private void nextLogout(Message logout) throws IOException, RejectLogon, FieldNo return; } + final boolean logoutInitiatedLocally = state.isLogoutSent(); state.setLogoutReceived(true); String msg; @@ -1413,11 +1469,15 @@ private void nextLogout(Message logout) throws IOException, RejectLogon, FieldNo if (getExpectedTargetNum() == logout.getHeader().getInt(MsgSeqNum.FIELD)) { state.incrNextTargetMsgSeqNum(); } - if (resetOnLogout) { + if (resetOnLogout || state.isResetStatePending()) { resetState(); } disconnect(msg, false); + + if (!state.isInitiator() && !logoutInitiatedLocally) { + setEnabled(true); + } } public void generateLogout() { @@ -1477,7 +1537,7 @@ private void nextSequenceReset(Message sequenceReset) throws IOException, Reject if (validateSequenceNumbers && sequenceReset.isSetField(NewSeqNo.FIELD)) { final int newSequence = sequenceReset.getInt(NewSeqNo.FIELD); - + stateListener.onSequenceResetReceived(sessionID, newSequence, isGapFill); getLog().onEvent( "Received SequenceReset FROM: " + getExpectedTargetNum() + " TO: " + newSequence); @@ -1502,7 +1562,7 @@ private void nextSequenceReset(Message sequenceReset) throws IOException, Reject } // QFJ-728: newSequence will be the seqnum of the next message so we // delete all older messages from the queue since they are effectively skipped. - state.dequeueMessagesUpTo(newSequence); + state.getMessageQueue().dequeueMessagesUpTo(newSequence); } else if (newSequence < getExpectedTargetNum()) { getLog().onErrorEvent( @@ -1666,9 +1726,10 @@ private void setRejectReason(Message reject, int field, String reason, private void generateBusinessReject(Message message, int err, int field) throws FieldNotFound, IOException { - final Message reject = messageFactory.create(sessionID.getBeginString(), - MsgType.BUSINESS_MESSAGE_REJECT); final Header header = message.getHeader(); + ApplVerID targetDefaultApplicationVersionID = getTargetDefaultApplicationVersionID(); + final Message reject = messageFactory.create(sessionID.getBeginString(), targetDefaultApplicationVersionID, + MsgType.BUSINESS_MESSAGE_REJECT); reject.reverseRoute(header); initializeHeader(reject.getHeader()); @@ -1682,8 +1743,8 @@ private void generateBusinessReject(Message message, int err, int field) throws final String reason = BusinessRejectReasonText.getMessage(err); setRejectReason(reject, field, reason, field != 0); getLog().onErrorEvent( - "Reject sent for message " + msgSeqNum + (reason != null ? (": " + reason) : "") - + (field != 0 ? (": tag=" + field) : "")); + "Reject sent for message number " + msgSeqNum + (reason != null ? (": " + reason) : "") + + (field != 0 ? (": tag=" + field) : "") + " Message [" + message + "]" + " Reject [" + reject + "]"); sendRaw(reject, 0); } @@ -1755,12 +1816,14 @@ private boolean verify(Message msg, boolean checkTooHigh, boolean checkTooLow) return false; } - if (checkTooHigh && isTargetTooHigh(msgSeqNum)) { - doTargetTooHigh(msg); - return false; - } else if (checkTooLow && isTargetTooLow(msgSeqNum)) { - doTargetTooLow(msg); - return false; + if(!state.isResetReceived()){ + if (checkTooHigh && isTargetTooHigh(msgSeqNum)) { + doTargetTooHigh(msg); + return false; + } else if (checkTooLow && isTargetTooLow(msgSeqNum)) { + doTargetTooLow(msg); + return false; + } } // Handle poss dup where msgSeq is as expected @@ -1777,6 +1840,7 @@ private boolean verify(Message msg, boolean checkTooHigh, boolean checkTooLow) getLog().onEvent( "ResendRequest for messages FROM " + range.getBeginSeqNo() + " TO " + range.getEndSeqNo() + " has been satisfied."); + stateListener.onResendRequestSatisfied(sessionID, range.getBeginSeqNo(), range.getEndSeqNo()); state.setResendRange(0, 0, 0); } } @@ -1893,8 +1957,10 @@ public void next() throws IOException { if ((now - lastSessionTimeCheck) >= 1000L) { lastSessionTimeCheck = now; if (!isSessionTime()) { - if (state.isResetNeeded()) { - reset(); // only reset if seq nums are != 1 + if (state.isResetNeeded() && !state.isResetStatePending()) { + reset(); // only reset if seq nums are != 1 and not isResetStatePending is set + } else if (state.isLogoutTimedOut()) { + disconnect("Timed out waiting for logout response", true); } return; // since we are outside of session time window } else { @@ -1932,18 +1998,18 @@ public void next() throws IOException { return; } - if (state.getHeartBeatInterval() == 0) { - return; - } - if (state.isLogoutTimedOut()) { disconnect("Timed out waiting for logout response", true); } + if (state.getHeartBeatInterval() == 0) { + return; + } + if (state.isTimedOut()) { if (!disableHeartBeatCheck) { disconnect("Timed out waiting for heartbeat", true); - stateListener.onHeartBeatTimeout(); + stateListener.onHeartBeatTimeout(sessionID); } else { LOG.warn("Heartbeat failure detected but deactivated"); } @@ -1951,7 +2017,7 @@ public void next() throws IOException { if (state.isTestRequestNeeded()) { generateTestRequest("TEST"); getLog().onEvent("Sent test request TEST"); - stateListener.onMissedHeartBeat(); + stateListener.onMissedHeartBeat(sessionID); } else if (state.isHeartBeatNeeded()) { generateHeartbeat(); } @@ -1999,10 +2065,8 @@ private boolean generateLogon() throws IOException { if (sessionID.isFIXT()) { logon.setField(DefaultApplVerID.FIELD, senderDefaultApplVerID); } - if (isStateRefreshNeeded(MsgType.LOGON)) { - getLog().onEvent("Refreshing message/state store at logon"); - getStore().refresh(); - stateListener.onRefresh(); + if (refreshOnLogon) { + refreshState(); } if (resetOnLogon) { resetState(); @@ -2020,17 +2084,19 @@ private boolean generateLogon() throws IOException { logon.setInt(NextExpectedMsgSeqNum.FIELD, nextExpectedMsgNum); state.setLastExpectedLogonNextSeqNum(nextExpectedMsgNum); } + + setLogonTags(logon); return sendRaw(logon, 0); } /** * 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 @@ -2064,25 +2130,20 @@ public void disconnect(String reason, boolean logError) throws IOException { logApplicationException("onLogout()", t); } - stateListener.onLogout(); + stateListener.onLogout(sessionID); } } finally { - // QFJ-457 now enabled again if acceptor - if (!state.isInitiator()) { - setEnabled(true); - } - state.setLogonReceived(false); state.setLogonSent(false); state.setLogoutSent(false); state.setLogoutReceived(false); state.setResetReceived(false); state.setResetSent(false); - state.clearQueue(); + state.getMessageQueue().clear(); state.clearLogoutReason(); state.setResendRange(0, 0); - if (resetOnDisconnect) { + if (resetOnDisconnect || state.isResetStatePending()) { resetState(); } } @@ -2091,6 +2152,9 @@ public void disconnect(String reason, boolean logError) throws IOException { private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException, InvalidMessage { + if (!enabled) { + throw new RejectLogon("Logon attempt when session is disabled"); + } // QFJ-357 // If this check is not done here, the Logon would be accepted and // immediately followed by a Logout (due to check in Session.next()). @@ -2101,10 +2165,8 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre // 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(); - stateListener.onRefresh(); + if (refreshOnLogon) { + refreshState(); } if (logon.isSetField(ResetSeqNumFlag.FIELD)) { @@ -2115,6 +2177,10 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre state.setResetReceived(true); } + if (!verify(logon, false, validateSequenceNumbers)) { + return; + } + if (state.isResetReceived()) { getLog().onEvent("Logon contains ResetSeqNumFlag=Y, resetting sequence numbers to 1"); if (!state.isResetSent()) { @@ -2131,9 +2197,6 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre resetState(); } - if (!verify(logon, false, validateSequenceNumbers)) { - return; - } // reset logout messages state.setLogoutReceived(false); @@ -2249,7 +2312,7 @@ private void nextLogon(Message logon) throws FieldNotFound, RejectLogon, Incorre } catch (final Throwable t) { logApplicationException("onLogon()", t); } - stateListener.onLogon(); + stateListener.onLogon(sessionID); lastSessionLogon = SystemTime.currentTimeMillis(); logonAttempts = 0; } @@ -2369,7 +2432,7 @@ private void nextQueued() throws FieldNotFound, RejectLogon, IncorrectDataFormat private boolean nextQueued(int num) throws FieldNotFound, RejectLogon, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType, IOException, InvalidMessage { - final Message msg = state.dequeue(num); + final Message msg = state.getMessageQueue().dequeue(num); if (msg != null) { getLog().onEvent("Processing queued message: " + num); @@ -2469,9 +2532,10 @@ private void sendResendRequest(String beginString, int msgSeqNum, int beginSeqNo initializeHeader(resendRequest.getHeader()); sendRaw(resendRequest, 0); getLog().onEvent("Sent ResendRequest FROM: " + beginSeqNo + " TO: " + (endSeqNo == 0 ? "infinity" : endSeqNo)); - state.setResendRange(beginSeqNo, msgSeqNum - 1, resendRequestChunkSize == 0 - ? 0 - : lastEndSeqNoSent); + int resendRangeEndSeqNum = msgSeqNum - 1; + int resendRangeCurrentSeqNum = resendRequestChunkSize == 0 ? 0 : lastEndSeqNoSent; + state.setResendRange(beginSeqNo, resendRangeEndSeqNum, resendRangeCurrentSeqNum); + stateListener.onResendRequestSent(sessionID, beginSeqNo, resendRangeEndSeqNum, resendRangeCurrentSeqNum); } private boolean validatePossDup(Message msg) throws FieldNotFound, IOException { @@ -2534,6 +2598,8 @@ private void generateLogon(Message otherLogon, int expectedTargetNum) throws Fie } else { getLog().onEvent("Responding to Logon request"); } + + setLogonTags(logon); sendRaw(logon, 0); state.setLogonSent(true); } @@ -2635,7 +2701,7 @@ private boolean sendRaw(Message message, int num) { } private void enqueueMessage(final Message msg, final int msgSeqNum) { - state.enqueue(msgSeqNum, msg); + state.getMessageQueue().enqueue(msgSeqNum, msg); getLog().onEvent("Enqueued at pos " + msgSeqNum + ": " + msg); } @@ -2645,9 +2711,10 @@ private void resetState() { } try { state.reset(); - stateListener.onReset(); + stateListener.onReset(sessionID); } finally { isResettingState.set(false); + state.setResetStatePending(false); } } @@ -2657,7 +2724,7 @@ private void resetState() { * information already is present). * * The returned status flag is included for - * compatibility with the JNI API but it's usefulness is questionable. + * compatibility with the JNI API but its usefulness is questionable. * In QuickFIX/J, the message is transmitted using asynchronous network I/O so the boolean * only indicates the message was successfully queued for transmission. An error could still * occur before the message data is actually sent. @@ -2666,6 +2733,30 @@ private void resetState() { * @return a status flag indicating whether the write to the network layer was successful. */ public boolean send(Message message) { + return send(message, this.allowPosDup); + } + + /** + * Send a message to a counterparty. Sequence numbers and information about the sender + * and target identification will be added automatically (or overwritten if that + * information already is present). + * + * The returned status flag is included for + * compatibility with the JNI API but its usefulness is questionable. + * In QuickFIX/J, the message is transmitted using asynchronous network I/O so the boolean + * only indicates the message was successfully queued for transmission. An error could still + * occur before the message data is actually sent. + * + * @param message the message to send + * @param allowPosDup whether to allow PossDupFlag and OrigSendingTime in the message + * @return a status flag indicating whether the write to the network layer was successful. + */ + public boolean send(Message message, boolean allowPosDup) { + // Send message as is if allowPosDup flag is set + if (allowPosDup) { + return sendRaw(message, 0); + } + message.getHeader().removeField(PossDupFlag.FIELD); message.getHeader().removeField(OrigSendingTime.FIELD); return sendRaw(message, 0); @@ -2785,7 +2876,7 @@ public boolean getRedundantResentRequestsAllowed() { } public boolean getRefreshOnLogon() { - return refreshMessageStoreAtLogon; + return refreshOnLogon; } public boolean getResetOnDisconnect() { @@ -2868,6 +2959,10 @@ public void removeStateListener(SessionStateListener listener) { stateListeners.removeListener(listener); } + public SessionStateListener getStateListener() { + return stateListener; + } + /** * @return the default application version ID for messages sent from this session */ @@ -2975,6 +3070,10 @@ public boolean isAllowedForSession(InetAddress remoteInetAddress) { || allowedRemoteAddresses.contains(remoteInetAddress); } + public void setAllowPosDup(boolean allowPosDup) { + this.allowPosDup = allowPosDup; + } + /** * Closes session resources and unregisters session. This is for internal * use and should typically not be called by an user application. @@ -3004,4 +3103,21 @@ private String getMessageToLog(final Message message) { return (message.toRawString() != null ? message.toRawString() : message.toString()); } + private void setLogonTags(final Message logon) { + for (StringField field : logonTags) { + if (dataDictionaryProvider != null + && dataDictionaryProvider.getSessionDataDictionary(sessionID.getBeginString()).isHeaderField(field.getTag())) { + logon.getHeader().setField(field); + continue; + } + logon.setField(field); + } + } + + private void refreshState() throws IOException { + getLog().onEvent("Refreshing message/state store on Logon"); + getStore().refresh(); + stateListener.onRefresh(sessionID); + } + } diff --git a/quickfixj-core/src/main/java/quickfix/SessionSettings.java b/quickfixj-core/src/main/java/quickfix/SessionSettings.java index 1cbf4200f5..3765c35c1f 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionSettings.java +++ b/quickfixj-core/src/main/java/quickfix/SessionSettings.java @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory; import quickfix.field.converter.BooleanConverter; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -47,6 +48,7 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Settings for sessions. Settings are grouped by FIX version and target company @@ -86,34 +88,45 @@ public class SessionSettings { // problems with moving configuration files between *nix and Windows. private static final String NEWLINE = "\r\n"; - private Properties variableValues = System.getProperties(); + private Properties variableValues; /** * Creates an empty session settings object. */ public SessionSettings() { + this(System.getProperties()); + } + + /** + * Creates an empty session settings object with custom source of variable values in the settings. + * @param variableValues custom source of variable values in the settings + */ + public SessionSettings(Properties variableValues) { sections.put(DEFAULT_SESSION_ID, new Properties()); + this.variableValues = variableValues; + } + + /** + * Loads session settings from a file with custom source of variable values in the settings. + * + * @param filename the path to the file containing the session settings + * @param variableValues custom source of variable values in the settings + * @throws ConfigError when file could not be loaded + */ + public SessionSettings(String filename, Properties variableValues) throws ConfigError { + this(variableValues); + loadFromFile(filename); } /** * Loads session settings from a file. * * @param filename the path to the file containing the session settings - * @throws quickfix.ConfigError when file could not be loaded + * @throws ConfigError when file could not be loaded */ public SessionSettings(String filename) throws ConfigError { this(); - try (InputStream in = getClass().getClassLoader().getResourceAsStream(filename)) { - if (in != null) { - load(in); - } else { - try (InputStream in2 = new FileInputStream(filename)) { - load(in2); - } - } - } catch (final IOException ex) { - throw new ConfigError(ex.getMessage()); - } + loadFromFile(filename); } /** @@ -127,6 +140,41 @@ public SessionSettings(InputStream stream) throws ConfigError { load(stream); } + /** + * Loads session settings from an input stream with custom source of variable values in the settings. + * + * @param stream the input stream + * @param variableValues custom source of variable values in the settings + * @throws ConfigError + */ + public SessionSettings(InputStream stream, Properties variableValues) throws ConfigError { + this(variableValues); + load(stream); + } + + /** + * Loads session settings from a list of strings. + * + * @param listValues the list of strings + * @throws ConfigError + */ + public SessionSettings(List listValues) throws ConfigError { + this(); + loadFromList(listValues); + } + + /** + * Loads session settings from a list of strings with custom source of variable values in the settings. + * + * @param listValues the list of strings + * @param variableValues custom source of variable values in the settings + * @throws ConfigError + */ + public SessionSettings(List listValues, Properties variableValues) throws ConfigError { + this(variableValues); + loadFromList(listValues); + } + /** * Gets a string from the default section of the settings. * @@ -138,6 +186,13 @@ public String getString(String key) throws ConfigError { return getString(DEFAULT_SESSION_ID, key); } + /** + * Gets a string from the default section if present or use default value. + */ + public String getStringOrDefault(String key, String defaultValue) throws ConfigError { + return isSetting(key) ? getString(key) : defaultValue; + } + /** * Get a settings string. * @@ -154,6 +209,13 @@ public String getString(SessionID sessionID, String key) throws ConfigError { return value; } + /** + * Get a settings string if present or use default value. + */ + public String getStringOrDefault(SessionID sessionID, String key, String defaultValue) throws ConfigError { + return isSetting(sessionID, key) ? getString(sessionID, key) : defaultValue; + } + /** * Return the settings for a session as a Properties object. * @@ -195,7 +257,6 @@ public Properties getSessionProperties(SessionID sessionID) throws ConfigError { * Returns the defaults for the session-level settings. * * @return the default properties - * @throws ConfigError */ public Properties getDefaultProperties() { try { @@ -218,6 +279,13 @@ public long getLong(String key) throws ConfigError, FieldConvertError { return getLong(DEFAULT_SESSION_ID, key); } + /** + * Gets a long from the default section of settings if present or use default value. + */ + public long getLongOrDefault(String key, long defaultValue) throws ConfigError, FieldConvertError { + return isSetting(key) ? getLong(key) : defaultValue; + } + /** * Get a settings value as a long integer. * @@ -235,6 +303,13 @@ public long getLong(SessionID sessionID, String key) throws ConfigError, FieldCo } } + /** + * Get an existing settings value as a long if present or use default value. + */ + public long getLongOrDefault(SessionID sessionID, String key, long defaultValue) throws ConfigError, FieldConvertError { + return isSetting(sessionID, key) ? getLong(sessionID, key) : defaultValue; + } + /** * Gets an int from the default section of settings. * @@ -247,13 +322,20 @@ public int getInt(String key) throws ConfigError, FieldConvertError { return getInt(DEFAULT_SESSION_ID, key); } + /** + * Gets an int from the default section of settings if present or use default value. + */ + public int getIntOrDefault(String key, int defaultValue) throws ConfigError, FieldConvertError { + return isSetting(key) ? getInt(key) : defaultValue; + } + /** * Get a settings value as an integer. * * @param sessionID the session ID * @param key the settings key * @return the long integer value for the setting - * @throws ConfigError configurion error, probably a missing setting. + * @throws ConfigError configuration error, probably a missing setting. * @throws FieldConvertError error during field type conversion. */ public int getInt(SessionID sessionID, String key) throws ConfigError, FieldConvertError { @@ -264,6 +346,13 @@ public int getInt(SessionID sessionID, String key) throws ConfigError, FieldConv } } + /** + * Get an existing settings value as an integer if present or use default value. + */ + public int getIntOrDefault(SessionID sessionID, String key, int defaultValue) throws ConfigError, FieldConvertError { + return isSetting(sessionID, key) ? getInt(sessionID, key) : defaultValue; + } + private Properties getOrCreateSessionProperties(SessionID sessionID) { return sections.computeIfAbsent(sessionID, k -> new Properties(sections.get(DEFAULT_SESSION_ID))); } @@ -318,12 +407,8 @@ public boolean getBool(String key) throws ConfigError, FieldConvertError { * @throws ConfigError configuration error, probably a missing setting. * @throws FieldConvertError error during field type conversion. */ - public boolean getBool(SessionID sessionID, String key) throws ConfigError, FieldConvertError { - try { - return BooleanConverter.convert(getString(sessionID, key)); - } catch (final FieldConvertError e) { - throw new ConfigError(e); - } + public boolean getBool(SessionID sessionID, String key) throws FieldConvertError, ConfigError { + return BooleanConverter.convert(getString(sessionID, key)); } /** @@ -378,6 +463,26 @@ public Iterator sectionIterator() { return nondefaultSessions.iterator(); } + private void loadFromFile(String filename) throws ConfigError { + try (InputStream in = getClass().getClassLoader().getResourceAsStream(filename)) { + if (in != null) { + load(in); + } else { + try (InputStream in2 = new FileInputStream(filename)) { + load(in2); + } + } + } catch (final IOException ex) { + throw new ConfigError(ex.getMessage()); + } + } + + private void loadFromList(List listValues) throws ConfigError { + byte[] bytes = listValues.stream().collect(Collectors.joining(System.lineSeparator())).getBytes(); + InputStream in = new ByteArrayInputStream(bytes); + load(in); + } + private void load(InputStream inputStream) throws ConfigError { try (final Reader reader = new InputStreamReader(inputStream)) { Properties currentSection = null; @@ -405,8 +510,7 @@ private void load(InputStream inputStream) throws ConfigError { } storeSection(currentSectionId, currentSection); } catch (final IOException e) { - final ConfigError configError = new ConfigError(e.getMessage()); - throw configError; + throw new ConfigError(e.getMessage()); } } diff --git a/quickfixj-core/src/main/java/quickfix/SessionState.java b/quickfixj-core/src/main/java/quickfix/SessionState.java index 6d894e7b75..b897b4de1b 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionState.java +++ b/quickfixj-core/src/main/java/quickfix/SessionState.java @@ -23,8 +23,7 @@ import java.util.Calendar; import java.util.Collection; import java.util.Date; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -40,6 +39,9 @@ public final class SessionState { // MessageStore implementation must be thread safe private final MessageStore messageStore; + // MessageQueue implementation must be thread safe + private final MessageQueue messageQueue; + private final Lock senderMsgSeqNumLock = new ReentrantLock(); private final Lock targetMsgSeqNumLock = new ReentrantLock(); @@ -56,6 +58,7 @@ public final class SessionState { private long lastSentTime; private long lastReceivedTime; private final double testRequestDelayMultiplier; + private final double heartBeatTimeoutMultiplier; private long heartBeatMillis = Long.MAX_VALUE; private int heartBeatInterval; @@ -64,6 +67,12 @@ public final class SessionState { private boolean resetReceived; private String logoutReason; + /** + * Indicates whether resetting the internal state is still pending. + * Used when resetting sequence numbers after Logout. + */ + private boolean resetStatePending = false; + /* * If this is anything other than zero it's the value of the 789/NextExpectedMsgSeqNum tag in the last Logon message sent. * It is used to determine if the recipient has enough information (assuming they support 789) to avoid the need @@ -72,17 +81,16 @@ 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<>(); - public SessionState(Object lock, Log log, int heartBeatInterval, boolean initiator, MessageStore messageStore, - double testRequestDelayMultiplier) { + MessageQueue messageQueue, double testRequestDelayMultiplier, double heartBeatTimeoutMultiplier) { this.lock = lock; this.initiator = initiator; this.messageStore = messageStore; + this.messageQueue = messageQueue; setHeartBeatInterval(heartBeatInterval); this.log = log == null ? new NullLog() : log; this.testRequestDelayMultiplier = testRequestDelayMultiplier; + this.heartBeatTimeoutMultiplier = heartBeatTimeoutMultiplier; } public int getHeartBeatInterval() { @@ -94,13 +102,7 @@ public int getHeartBeatInterval() { public void setHeartBeatInterval(int heartBeatInterval) { synchronized (lock) { this.heartBeatInterval = heartBeatInterval; - } - setHeartBeatMillis(heartBeatInterval * 1000L); - } - - private void setHeartBeatMillis(long heartBeatMillis) { - synchronized (lock) { - this.heartBeatMillis = heartBeatMillis; + this.heartBeatMillis = TimeUnit.SECONDS.toMillis(heartBeatInterval); } } @@ -258,6 +260,10 @@ public MessageStore getMessageStore() { return messageStore; } + public MessageQueue getMessageQueue() { + return messageQueue; + } + private int getTestRequestCounter() { synchronized (lock) { return testRequestCounter; @@ -292,7 +298,7 @@ private long timeSinceLastReceivedMessage() { public boolean isTimedOut() { long millisSinceLastReceivedTime = timeSinceLastReceivedMessage(); - return millisSinceLastReceivedTime >= 2.4 * getHeartBeatMillis(); + return millisSinceLastReceivedTime >= (1 + heartBeatTimeoutMultiplier) * getHeartBeatMillis(); } public boolean set(int sequence, String message) throws IOException { @@ -303,37 +309,6 @@ public void get(int first, int last, Collection messages) throws IOExcep messageStore.get(first, last, messages); } - public void enqueue(int sequence, Message message) { - messageQueue.put(sequence, message); - } - - public Message dequeue(int sequence) { - return messageQueue.remove(sequence); - } - - /** - * Remove messages from messageQueue up to a given sequence number. - * - * @param seqnum up to which sequence number messages should be deleted - */ - public void dequeueMessagesUpTo(int seqnum) { - for (int i = 1; i < seqnum; i++) { - dequeue(i); - } - } - - public Message getNextQueuedMessage() { - return !messageQueue.isEmpty() ? messageQueue.values().iterator().next() : null; - } - - public Collection getQueuedSeqNums() { - return messageQueue.keySet(); - } - - public void clearQueue() { - messageQueue.clear(); - } - public void lockSenderMsgSeqNum() { senderMsgSeqNumLock.lock(); } @@ -436,6 +411,19 @@ public void setResetSent(boolean resetSent) { this.resetSent = resetSent; } } + + public boolean isResetStatePending() { + synchronized (lock) { + return resetStatePending; + } + } + + public void setResetStatePending(boolean resetStatePending) { + synchronized (lock) { + this.resetStatePending = resetStatePending; + } + } + /** * No actual resend request has occurred but at logon we populated tag 789 so that the other side knows we diff --git a/quickfixj-core/src/main/java/quickfix/SessionStateListener.java b/quickfixj-core/src/main/java/quickfix/SessionStateListener.java index 0553b32078..4c1f0ea594 100644 --- a/quickfixj-core/src/main/java/quickfix/SessionStateListener.java +++ b/quickfixj-core/src/main/java/quickfix/SessionStateListener.java @@ -16,25 +16,93 @@ * Contact ask@quickfixengine.org if any conditions of this licensing * are not clear to you. ******************************************************************************/ - package quickfix; public interface SessionStateListener { - void onConnect(); + /** + * Called when connection has been established. + */ + default void onConnect(SessionID sessionID) { + } + + /** + * Called when Exception occurs during connection establishment. + * + * @param sessionID affected SessionID + * @param exception thrown Exception + */ + default void onConnectException(SessionID sessionID, Exception exception) { + } + + /** + * Called when connection has been disconnected. + */ + default void onDisconnect(SessionID sessionID) { + } + + /** + * Called when session has been logged on. + */ + default void onLogon(SessionID sessionID) { + } - void onDisconnect(); + /** + * Called when session has been logged out. + */ + default void onLogout(SessionID sessionID) { + } - void onLogon(); + /** + * Called when message store gets reset. + */ + default void onReset(SessionID sessionID) { + } - void onLogout(); + /** + * Called when message store gets refreshed on Logon. + */ + default void onRefresh(SessionID sessionID) { + } - void onReset(); + /** + * Called when TestRequest is sent out due to missed Heartbeat. + */ + default void onMissedHeartBeat(SessionID sessionID) { + } - void onRefresh(); + /** + * Called when Heartbeat timeout has been detected. + */ + default void onHeartBeatTimeout(SessionID sessionID) { + } - void onMissedHeartBeat(); + /** + * Called when ResendRequest has been sent out. + * + * @param beginSeqNo first seqnum that gets requested + * @param endSeqNo last seqnum that gets requested + * @param currentEndSeqNo last seqnum of range that gets requested on + * chunked ResendRequests + */ + default void onResendRequestSent(SessionID sessionID, int beginSeqNo, int endSeqNo, int currentEndSeqNo) { + } - void onHeartBeatTimeout(); + /** + * Called when SequenceReset has been received. + * + * @param newSeqNo NewSeqNo from SequenceReset + * @param gapFillFlag GapFillFlag from SequenceReset + */ + default void onSequenceResetReceived(SessionID sessionID, int newSeqNo, boolean gapFillFlag) { + } + /** + * Called when a received ResendRequest has been satisfied. + * + * @param beginSeqNo first seqnum that was requested + * @param endSeqNo last seqnum that was requested + */ + default void onResendRequestSatisfied(SessionID sessionID, int beginSeqNo, int endSeqNo) { + } } diff --git a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java index 49f27d7f15..23a2769448 100644 --- a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java @@ -19,6 +19,7 @@ package quickfix; +import java.util.concurrent.atomic.AtomicBoolean; import quickfix.mina.EventHandlingStrategy; import quickfix.mina.SingleThreadedEventHandlingStrategy; import quickfix.mina.acceptor.AbstractSocketAcceptor; @@ -28,7 +29,7 @@ * sessions. */ public class SocketAcceptor extends AbstractSocketAcceptor { - private volatile Boolean isStarted = Boolean.FALSE; + private final AtomicBoolean isStarted = new AtomicBoolean(false); private final SingleThreadedEventHandlingStrategy eventHandlingStrategy; private SocketAcceptor(Builder builder) throws ConfigError { @@ -103,30 +104,30 @@ public void start() throws ConfigError, RuntimeError { } private void initialize() throws ConfigError { - if (isStarted.equals(Boolean.FALSE)) { - eventHandlingStrategy.setExecutor(longLivedExecutor); - startAcceptingConnections(); - eventHandlingStrategy.blockInThread(); - isStarted = Boolean.TRUE; - } else { - log.warn("Ignored attempt to start already running SocketAcceptor."); + synchronized (isStarted) { + if (isStarted.compareAndSet(false, true)) { + eventHandlingStrategy.setExecutor(longLivedExecutor); + startAcceptingConnections(); + eventHandlingStrategy.blockInThread(); + } } } @Override public void stop(boolean forceDisconnect) { - if (isStarted.equals(Boolean.TRUE)) { - try { - logoutAllSessions(forceDisconnect); - stopAcceptingConnections(); - stopSessionTimer(); - } finally { + synchronized (isStarted) { + if (isStarted.compareAndSet(true, false)) { try { - eventHandlingStrategy.stopHandlingMessages(true); + logoutAllSessions(forceDisconnect); + stopAcceptingConnections(); + stopSessionTimer(); } finally { - Session.unregisterSessions(getSessions(), true); - clearConnectorSessions(); - isStarted = Boolean.FALSE; + try { + eventHandlingStrategy.stopHandlingMessages(true); + } finally { + Session.unregisterSessions(getSessions(), true); + clearConnectorSessions(); + } } } } diff --git a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java index 3502ae1467..fca1b0d089 100644 --- a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java @@ -19,6 +19,7 @@ package quickfix; +import java.util.concurrent.atomic.AtomicBoolean; import quickfix.mina.EventHandlingStrategy; import quickfix.mina.SingleThreadedEventHandlingStrategy; import quickfix.mina.initiator.AbstractSocketInitiator; @@ -28,12 +29,12 @@ * sessions. */ public class SocketInitiator extends AbstractSocketInitiator { - private volatile Boolean isStarted = Boolean.FALSE; + private final AtomicBoolean isStarted = new AtomicBoolean(false); private final SingleThreadedEventHandlingStrategy eventHandlingStrategy; private SocketInitiator(Builder builder) throws ConfigError { super(builder.application, builder.messageStoreFactory, builder.settings, - builder.logFactory, builder.messageFactory); + builder.logFactory, builder.messageFactory, builder.numReconnectThreads); if (builder.queueCapacity >= 0) { eventHandlingStrategy @@ -49,9 +50,17 @@ public static Builder newBuilder() { } public static final class Builder extends AbstractSessionConnectorBuilder { + + int numReconnectThreads = 3; + private Builder() { super(Builder.class); } + + public Builder withReconnectThreads(int numReconnectThreads) throws ConfigError { + this.numReconnectThreads = numReconnectThreads; + return this; + } @Override protected SocketInitiator doBuild() throws ConfigError { @@ -112,33 +121,30 @@ public void start() throws ConfigError, RuntimeError { } private void initialize() throws ConfigError { - if (isStarted.equals(Boolean.FALSE)) { - eventHandlingStrategy.setExecutor(longLivedExecutor); - createSessionInitiators(); - for (Session session : getSessionMap().values()) { - Session.registerSession(session); + synchronized (isStarted) { + if (isStarted.compareAndSet(false, true)) { + eventHandlingStrategy.setExecutor(longLivedExecutor); + createSessionInitiators(); + startInitiators(); + eventHandlingStrategy.blockInThread(); } - startInitiators(); - eventHandlingStrategy.blockInThread(); - isStarted = Boolean.TRUE; - } else { - log.warn("Ignored attempt to start already running SocketInitiator."); } } @Override public void stop(boolean forceDisconnect) { - if (isStarted.equals(Boolean.TRUE)) { - try { - logoutAllSessions(forceDisconnect); - stopInitiators(); - } finally { + synchronized (isStarted) { + if (isStarted.compareAndSet(true, false)) { try { - eventHandlingStrategy.stopHandlingMessages(true); + logoutAllSessions(forceDisconnect); + stopInitiators(); } finally { - Session.unregisterSessions(getSessions(), true); - clearConnectorSessions(); - isStarted = Boolean.FALSE; + try { + eventHandlingStrategy.stopHandlingMessages(true); + } finally { + Session.unregisterSessions(getSessions(), true); + clearConnectorSessions(); + } } } } diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java index a97d55c2c8..2a01b8b3c5 100644 --- a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java @@ -31,7 +31,7 @@ public class ThreadedSocketInitiator extends AbstractSocketInitiator { private ThreadedSocketInitiator(Builder builder) throws ConfigError { super(builder.application, builder.messageStoreFactory, builder.settings, - builder.logFactory, builder.messageFactory); + builder.logFactory, builder.messageFactory, builder.numReconnectThreads); if (builder.queueCapacity >= 0) { eventHandlingStrategy @@ -47,10 +47,18 @@ public static Builder newBuilder() { } public static final class Builder extends AbstractSessionConnectorBuilder { + + int numReconnectThreads = 3; + private Builder() { super(Builder.class); } + public Builder withReconnectThreads(int numReconnectThreads) throws ConfigError { + this.numReconnectThreads = numReconnectThreads; + return this; + } + @Override protected ThreadedSocketInitiator doBuild() throws ConfigError { return new ThreadedSocketInitiator(this); diff --git a/quickfixj-core/src/main/java/quickfix/field/converter/IntConverter.java b/quickfixj-core/src/main/java/quickfix/field/converter/IntConverter.java deleted file mode 100644 index d256ae9be5..0000000000 --- a/quickfixj-core/src/main/java/quickfix/field/converter/IntConverter.java +++ /dev/null @@ -1,60 +0,0 @@ -/******************************************************************************* - * Copyright (c) quickfixengine.org All rights reserved. - * - * This file is part of the QuickFIX FIX Engine - * - * This file may be distributed under the terms of the quickfixengine.org - * license as defined by quickfixengine.org and appearing in the file - * LICENSE included in the packaging of this file. - * - * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING - * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE. - * - * See http://www.quickfixengine.org/LICENSE for licensing information. - * - * Contact ask@quickfixengine.org if any conditions of this licensing - * are not clear to you. - ******************************************************************************/ - -package quickfix.field.converter; - -import quickfix.FieldConvertError; - -/** - * Convert between an integer and a String - */ -public final class IntConverter { - - /** - * Convert and integer to a String - * - * @param i the integer to convert - * @return the String representing the integer - * @see java.lang.Long#toString(long) - */ - public static String convert(int i) { - return Long.toString(i); - } - - /** - * Convert a String to an integer. - * - * @param value the String to convert - * @return the converted integer - * @throws FieldConvertError raised if the String does not represent a valid integer - * @see java.lang.Integer#parseInt(String) - */ - public static int convert(String value) throws FieldConvertError { - try { - for (int i = 0; i < value.length(); i++) { - if (!Character.isDigit(value.charAt(i)) && !(i == 0 && value.charAt(i) == '-')) { - throw new FieldConvertError("invalid integral value: " + value); - } - } - return Integer.parseInt(value); - } catch (NumberFormatException e) { - throw new FieldConvertError("invalid integral value: " + value + ": " + e); - } - } -} diff --git a/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java b/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java index 727850970a..79203dccc6 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java +++ b/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java @@ -26,14 +26,18 @@ import org.apache.mina.filter.codec.ProtocolDecoderException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import quickfix.ConfigError; +import quickfix.FieldConvertError; import quickfix.InvalidMessage; import quickfix.Log; import quickfix.LogUtil; import quickfix.Message; import quickfix.MessageUtils; -import static quickfix.MessageUtils.parse; import quickfix.Session; import quickfix.SessionID; +import quickfix.SessionSettings; + +import static quickfix.MessageSessionUtils.parse; /** * Abstract class used for acceptor and initiator IO handlers. @@ -42,10 +46,21 @@ public abstract class AbstractIoHandler extends IoHandlerAdapter { protected final Logger log = LoggerFactory.getLogger(getClass()); private final NetworkingOptions networkingOptions; private final EventHandlingStrategy eventHandlingStrategy; + private final SessionSettings sessionSettings; + private boolean logMessageWhenSessionNotFound; - public AbstractIoHandler(NetworkingOptions options, EventHandlingStrategy eventHandlingStrategy) { + public AbstractIoHandler(SessionSettings settings, NetworkingOptions options, EventHandlingStrategy eventHandlingStrategy) { + sessionSettings = settings; networkingOptions = options; this.eventHandlingStrategy = eventHandlingStrategy; + logMessageWhenSessionNotFound = true; + try { + if (sessionSettings.isSetting(Session.SETTING_LOG_MESSAGE_WHEN_SESSION_NOT_FOUND)) { + logMessageWhenSessionNotFound = sessionSettings.getBool(Session.SETTING_LOG_MESSAGE_WHEN_SESSION_NOT_FOUND); + } + } catch (ConfigError | FieldConvertError e) { + // ignore + } } @Override @@ -87,7 +102,7 @@ public void exceptionCaught(IoSession ioSession, Throwable cause) throws Excepti quickFixSession.disconnect(reason, true); } else { log.error(reason, cause); - ioSession.closeNow(); + ioSession.closeOnFlush(); } } finally { ioSession.setAttribute(SessionConnector.QFJ_RESET_IO_CONNECTOR, Boolean.TRUE); @@ -102,13 +117,7 @@ public void exceptionCaught(IoSession ioSession, Throwable cause) throws Excepti } @Override - public void sessionCreated(IoSession ioSession) throws Exception { - super.sessionCreated(ioSession); - networkingOptions.apply(ioSession); - } - - @Override - public void sessionClosed(IoSession ioSession) throws Exception { + public void sessionClosed(IoSession ioSession) { try { Session quickFixSession = findQFSession(ioSession); if (quickFixSession != null) { @@ -118,7 +127,7 @@ public void sessionClosed(IoSession ioSession) throws Exception { throw e; } finally { ioSession.removeAttribute(SessionConnector.QF_SESSION); - ioSession.closeNow(); + ioSession.closeOnFlush(); } } @@ -151,7 +160,11 @@ public void messageReceived(IoSession ioSession, Object message) throws Exceptio } } } else { - log.error("Disconnecting; received message for unknown session: {}", messageString); + if (logMessageWhenSessionNotFound) { + log.error("Disconnecting; received message for unknown session: {}", messageString); + } else { + log.error("Disconnecting; received message for unknown session. Remote SessionID: {}", remoteSessionID); + } ioSession.closeNow(); } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java b/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java index b653f15eb9..f525568064 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java +++ b/quickfixj-core/src/main/java/quickfix/mina/NetworkingOptions.java @@ -19,7 +19,7 @@ package quickfix.mina; -import org.apache.mina.core.session.IoSession; +import org.apache.mina.core.service.IoService; import org.apache.mina.core.session.IoSessionConfig; import org.apache.mina.transport.socket.SocketSessionConfig; import org.slf4j.Logger; @@ -28,7 +28,6 @@ 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; @@ -126,8 +125,8 @@ private Integer getInteger(Properties properties, String key, Integer defaultVal return value; } - public void apply(IoSession session) throws SocketException { - IoSessionConfig sessionConfig = session.getConfig(); + public void apply(IoService service) { + IoSessionConfig sessionConfig = service.getSessionConfig(); if (sessionConfig instanceof SocketSessionConfig) { SocketSessionConfig socketSessionConfig = (SocketSessionConfig) sessionConfig; if (keepAlive != null) { diff --git a/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java b/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java index 0e9bfaf6d5..13b88f0eec 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ProtocolFactory.java @@ -21,8 +21,6 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.util.ArrayList; -import java.util.List; import java.util.HashMap; @@ -36,7 +34,6 @@ 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; @@ -141,13 +138,6 @@ public static ProxyConnector createIoProxyConnector(SocketConnector socketConnec } 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; diff --git a/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java b/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java index cf3fb8cc1e..f58455343a 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java +++ b/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java @@ -71,7 +71,7 @@ public abstract class SessionConnector implements Connector { private final Map sessions = new ConcurrentHashMap<>(); private final SessionSettings settings; private final SessionFactory sessionFactory; - private final static ScheduledExecutorService scheduledExecutorService = Executors + private static final ScheduledExecutorService SCHEDULED_EXECUTOR = Executors .newSingleThreadScheduledExecutor(new QFTimerThreadFactory()); private ScheduledFuture sessionTimerFuture; private IoFilterChainBuilder ioFilterChainBuilder; @@ -272,6 +272,7 @@ protected void waitForLogout() { Thread.sleep(100L); } catch (InterruptedException e) { log.error(e.getMessage(), e); + Thread.currentThread().interrupt(); } final long elapsed = System.currentTimeMillis() - start; Iterator sessionItr = loggedOnSessions.iterator(); @@ -314,7 +315,7 @@ protected void startSessionTimer() { if (shortLivedExecutor != null) { timerTask = new DelegatingTask(timerTask, shortLivedExecutor); } - sessionTimerFuture = scheduledExecutorService.scheduleAtFixedRate(timerTask, 0, 1000L, + sessionTimerFuture = SCHEDULED_EXECUTOR.scheduleAtFixedRate(timerTask, 0, 1000L, TimeUnit.MILLISECONDS); log.info("SessionTimer started"); } @@ -335,10 +336,11 @@ boolean checkSessionTimerRunning() { } protected ScheduledExecutorService getScheduledExecutorService() { - return scheduledExecutorService; + return SCHEDULED_EXECUTOR; } private class SessionTimerTask implements Runnable { + @Override public void run() { try { for (Session session : sessions.values()) { @@ -374,6 +376,7 @@ public void run() { try { delegate.await(); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } @@ -407,6 +410,7 @@ void await() throws InterruptedException { private static class QFTimerThreadFactory implements ThreadFactory { + @Override public Thread newThread(Runnable runnable) { Thread thread = new Thread(runnable, "QFJ Timer"); thread.setDaemon(true); @@ -447,9 +451,10 @@ public static void closeManagedSessionsAndDispose(IoService ioService, boolean a completed = closeFuture.await(1000, TimeUnit.MILLISECONDS); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); - } - if (!completed) { - logger.warn("Could not close IoSession {}", ioSession); + } finally { + if (!completed) { + logger.warn("Could not close IoSession {}", ioSession); + } } } } @@ -458,4 +463,16 @@ public static void closeManagedSessionsAndDispose(IoService ioService, boolean a } } + protected boolean isContinueInitOnError() { + boolean continueInitOnError = false; + if (settings.isSetting(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR)) { + try { + continueInitOnError = settings.getBool(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR); + } catch (ConfigError | FieldConvertError ex) { + // ignore and return default + } + } + return continueInitOnError; + } + } diff --git a/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java b/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java index 8d2e88d3ea..4d34d5ca97 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java +++ b/quickfixj-core/src/main/java/quickfix/mina/SingleThreadedEventHandlingStrategy.java @@ -76,7 +76,7 @@ public void onMessage(Session quickfixSession, Message message) { queueTracker.put(new SessionMessageEvent(quickfixSession, message)); } catch (InterruptedException e) { isStopped = true; - throw new RuntimeException(e); + Thread.currentThread().interrupt(); } } @@ -194,9 +194,12 @@ public void stopHandlingMessages(boolean join) { stopHandlingMessages(); if (join) { try { - messageProcessingThread.join(); + if (messageProcessingThread != null) { + messageProcessingThread.join(); + } } catch (InterruptedException e) { - sessionConnector.log.error("{} interrupted.", MESSAGE_PROCESSOR_THREAD_NAME); + sessionConnector.log.warn("{} interrupted.", MESSAGE_PROCESSOR_THREAD_NAME); + Thread.currentThread().interrupt(); } } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java b/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java index dce95ec787..90d977a9ce 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ThreadPerSessionEventHandlingStrategy.java @@ -203,6 +203,7 @@ public void enqueue(Message message) { queueTracker.put(message); } catch (final InterruptedException e) { quickfixSession.getLog().onErrorEvent(e.toString()); + Thread.currentThread().interrupt(); } } @@ -227,6 +228,7 @@ void doRun() { LogUtil.logThrowable(quickfixSession.getSessionID(), "Message dispatcher interrupted", e); stopping = true; + Thread.currentThread().interrupt(); } catch (final Throwable e) { LogUtil.logThrowable(quickfixSession.getSessionID(), "Error during message processing", e); diff --git a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java index a949bffe37..93a10c98c9 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java @@ -23,6 +23,7 @@ import org.apache.mina.core.buffer.SimpleBufferAllocator; import org.apache.mina.core.service.IoAcceptor; import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.filter.ssl.SslFilter; import quickfix.Acceptor; import quickfix.Application; import quickfix.ConfigError; @@ -45,10 +46,10 @@ import quickfix.mina.message.FIXProtocolCodecFactory; import quickfix.mina.ssl.SSLConfig; import quickfix.mina.ssl.SSLContextFactory; -import quickfix.mina.ssl.SSLFilter; import quickfix.mina.ssl.SSLSupport; import javax.net.ssl.SSLContext; +import java.io.IOException; import java.net.SocketAddress; import java.security.GeneralSecurityException; import java.util.Collection; @@ -56,6 +57,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** * Abstract base class for socket acceptors. @@ -64,7 +67,7 @@ public abstract class AbstractSocketAcceptor extends SessionConnector implements private final Map sessionProviders = new HashMap<>(); private final SessionFactory sessionFactory; private final Map socketDescriptorForAddress = new HashMap<>(); - private final Map ioAcceptors = new HashMap<>(); + private final ConcurrentMap ioAcceptors = new ConcurrentHashMap<>(); protected AbstractSocketAcceptor(SessionSettings settings, SessionFactory sessionFactory) throws ConfigError { @@ -91,12 +94,13 @@ protected AbstractSocketAcceptor(Application application, // TODO SYNC Does this method really need synchronization? protected synchronized void startAcceptingConnections() throws ConfigError { - SocketAddress address = null; - try { - createSessions(getSettings()); - startSessionTimer(); + boolean continueInitOnError = isContinueInitOnError(); + createSessions(getSettings(), continueInitOnError); + startSessionTimer(); - for (AcceptorSocketDescriptor socketDescriptor : socketDescriptorForAddress.values()) { + SocketAddress address = null; + for (AcceptorSocketDescriptor socketDescriptor : socketDescriptorForAddress.values()) { + try { address = socketDescriptor.getAddress(); IoAcceptor ioAcceptor = getIoAcceptor(socketDescriptor); CompositeIoFilterChainBuilder ioFilterChainBuilder = new CompositeIoFilterChainBuilder(getIoFilterChainBuilder()); @@ -112,12 +116,14 @@ protected synchronized void startAcceptingConnections() throws ConfigError { ioAcceptor.setCloseOnDeactivation(false); ioAcceptor.bind(socketDescriptor.getAddress()); log.info("Listening for connections at {} for session(s) {}", address, socketDescriptor.getAcceptedSessions().keySet()); + } catch (IOException | GeneralSecurityException | ConfigError e) { + if (continueInitOnError) { + log.warn("error during session initialization for session(s) {}, continuing...", socketDescriptor.getAcceptedSessions().keySet(), e); + } else { + log.error("Cannot start acceptor session for {}, error: {}", address, e); + throw new RuntimeError(e); + } } - } catch (FieldConvertError e) { - throw new ConfigError(e); - } catch (Exception e) { - log.error("Cannot start acceptor session for {}, error: {}", address, e); - throw new RuntimeError(e); } } @@ -126,29 +132,29 @@ private void installSSL(AcceptorSocketDescriptor descriptor, log.info("Installing SSL filter for {}", descriptor.getAddress()); SSLConfig sslConfig = descriptor.getSslConfig(); SSLContext sslContext = SSLContextFactory.getInstance(sslConfig); - SSLFilter sslFilter = new SSLFilter(sslContext); - sslFilter.setUseClientMode(false); + SslFilter sslFilter = new SslFilter(sslContext); sslFilter.setNeedClientAuth(sslConfig.isNeedClientAuth()); - sslFilter.setCipherSuites(sslConfig.getEnabledCipherSuites() != null ? sslConfig.getEnabledCipherSuites() + sslFilter.setEnabledCipherSuites(sslConfig.getEnabledCipherSuites() != null ? sslConfig.getEnabledCipherSuites() : SSLSupport.getDefaultCipherSuites(sslContext)); sslFilter.setEnabledProtocols(sslConfig.getEnabledProtocols() != null ? sslConfig.getEnabledProtocols() : SSLSupport.getSupportedProtocols(sslContext)); ioFilterChainBuilder.addLast(SSLSupport.FILTER_NAME, sslFilter); } - private IoAcceptor getIoAcceptor(AcceptorSocketDescriptor socketDescriptor, boolean init) throws ConfigError { + private IoAcceptor getIoAcceptor(AcceptorSocketDescriptor socketDescriptor) throws ConfigError { int transportType = ProtocolFactory.getAddressTransportType(socketDescriptor.getAddress()); AcceptorSessionProvider sessionProvider = sessionProviders. computeIfAbsent(socketDescriptor.getAddress(), k -> new DefaultAcceptorSessionProvider(socketDescriptor.getAcceptedSessions())); IoAcceptor ioAcceptor = ioAcceptors.get(socketDescriptor); - if (ioAcceptor == null && init) { + if (ioAcceptor == null) { ioAcceptor = ProtocolFactory.createIoAcceptor(transportType); try { SessionSettings settings = getSettings(); - ioAcceptor.setHandler(new AcceptorIoHandler(sessionProvider, new NetworkingOptions( - settings.getDefaultProperties()), getEventHandlingStrategy())); + NetworkingOptions networkingOptions = new NetworkingOptions(settings.getDefaultProperties()); + networkingOptions.apply(ioAcceptor); + ioAcceptor.setHandler(new AcceptorIoHandler(sessionProvider, settings, networkingOptions, getEventHandlingStrategy())); } catch (FieldConvertError e) { throw new ConfigError(e); } @@ -157,12 +163,8 @@ private IoAcceptor getIoAcceptor(AcceptorSocketDescriptor socketDescriptor, bool return ioAcceptor; } - private IoAcceptor getIoAcceptor(AcceptorSocketDescriptor socketDescriptor) throws ConfigError { - return getIoAcceptor(socketDescriptor, true); - } - - private AcceptorSocketDescriptor getAcceptorSocketDescriptor(SessionSettings settings, - SessionID sessionID) throws ConfigError, FieldConvertError { + private void setupSession(SessionSettings settings, SessionID sessionID, boolean isTemplate, Map allSessions) + throws ConfigError, FieldConvertError { int acceptTransportType = ProtocolFactory.SOCKET; if (settings.isSetting(sessionID, Acceptor.SETTING_SOCKET_ACCEPT_PROTOCOL)) { try { @@ -207,31 +209,46 @@ && getSettings().getBool(sessionID, SSLSupport.SETTING_USE_SSL)) { socketDescriptorForAddress.put(acceptorAddress, descriptor); } - return descriptor; + if (!isTemplate) { + Session session = sessionFactory.create(sessionID, settings); + descriptor.acceptSession(session); + allSessions.put(sessionID, session); + } } private boolean equals(Object object1, Object object2) { return object1 == null ? object2 == null : object1.equals(object2); } - private void createSessions(SessionSettings settings) throws ConfigError, FieldConvertError { - HashMap allSessions = new HashMap<>(); + private void createSessions(SessionSettings settings, boolean continueInitOnError) throws ConfigError { + Map allSessions = new HashMap<>(); for (Iterator i = settings.sectionIterator(); i.hasNext();) { SessionID sessionID = i.next(); - String connectionType = settings.getString(sessionID, - SessionFactory.SETTING_CONNECTION_TYPE); - - boolean isTemplate = false; - if (settings.isSetting(sessionID, Acceptor.SETTING_ACCEPTOR_TEMPLATE)) { - isTemplate = settings.getBool(sessionID, Acceptor.SETTING_ACCEPTOR_TEMPLATE); - } + try { + String connectionType = null; + if (settings.isSetting(sessionID, SessionFactory.SETTING_CONNECTION_TYPE)) { + connectionType = settings.getString(sessionID, + SessionFactory.SETTING_CONNECTION_TYPE); + } - if (connectionType.equals(SessionFactory.ACCEPTOR_CONNECTION_TYPE)) { - AcceptorSocketDescriptor descriptor = getAcceptorSocketDescriptor(settings, sessionID); - if (!isTemplate) { - Session session = sessionFactory.create(sessionID, settings); - descriptor.acceptSession(session); - allSessions.put(sessionID, session); + if (SessionFactory.ACCEPTOR_CONNECTION_TYPE.equals(connectionType)) { + boolean isTemplate = false; + if (settings.isSetting(sessionID, Acceptor.SETTING_ACCEPTOR_TEMPLATE)) { + try { + isTemplate = settings.getBool(sessionID, Acceptor.SETTING_ACCEPTOR_TEMPLATE); + } catch (FieldConvertError | ConfigError ex) { + // ignore and use default + } + } + + setupSession(settings, sessionID, isTemplate, allSessions); + } + } catch (Throwable t) { + if (continueInitOnError) { + log.warn("error during session initialization for {}, continuing...", sessionID, t); + } else { + throw t instanceof ConfigError ? (ConfigError) t : new ConfigError( + "error during session initialization", t); } } } 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 3b73ba829b..ca3e5dab56 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AcceptorIoHandler.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AcceptorIoHandler.java @@ -23,8 +23,10 @@ import quickfix.Log; import quickfix.Message; import quickfix.MessageUtils; +import quickfix.Responder; import quickfix.Session; import quickfix.SessionID; +import quickfix.SessionSettings; import quickfix.field.ApplVerID; import quickfix.field.DefaultApplVerID; import quickfix.field.HeartBtInt; @@ -45,8 +47,10 @@ class AcceptorIoHandler extends AbstractIoHandler { private final AcceptorSessionProvider sessionProvider; public AcceptorIoHandler(AcceptorSessionProvider sessionProvider, - NetworkingOptions networkingOptions, EventHandlingStrategy eventHandlingStrategy) { - super(networkingOptions, eventHandlingStrategy); + SessionSettings sessionSettings, + NetworkingOptions networkingOptions, + EventHandlingStrategy eventHandlingStrategy) { + super(sessionSettings, networkingOptions, eventHandlingStrategy); this.sessionProvider = sessionProvider; this.eventHandlingStrategy = eventHandlingStrategy; } @@ -67,10 +71,12 @@ protected void processMessage(IoSession protocolSession, Message message) throws qfSession = sessionProvider.getSession(sessionID, eventHandlingStrategy.getSessionConnector()); if (qfSession != null) { final Log sessionLog = qfSession.getLog(); - if (qfSession.hasResponder()) { + Responder responder = qfSession.getResponder(); + if (responder != null) { // Session is already bound to another connection - sessionLog - .onErrorEvent("Multiple logons/connections for this session are not allowed"); + sessionLog.onErrorEvent("Multiple logons/connections for this session are not allowed." + + " Closing connection from " + protocolSession.getRemoteAddress() + + " since session is already established from " + responder.getRemoteAddress()); 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 fd3ea0aa20..84f9e142e9 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java @@ -47,10 +47,15 @@ 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 java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicInteger; /** * Abstract base class for socket initiators. @@ -58,7 +63,9 @@ 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 = ConcurrentHashMap.newKeySet(); + private final ScheduledExecutorService scheduledReconnectExecutor; + public static final String QFJ_RECONNECT_THREAD_PREFIX = "QFJ Reconnect Thread-"; protected AbstractSocketInitiator(Application application, MessageStoreFactory messageStoreFactory, SessionSettings settings, @@ -69,24 +76,43 @@ protected AbstractSocketInitiator(Application application, protected AbstractSocketInitiator(SessionSettings settings, SessionFactory sessionFactory) throws ConfigError { + this(settings, sessionFactory, 0); + } + + protected AbstractSocketInitiator(Application application, + MessageStoreFactory messageStoreFactory, SessionSettings settings, + LogFactory logFactory, MessageFactory messageFactory, int numReconnectThreads) throws ConfigError { + this(settings, new DefaultSessionFactory(application, messageStoreFactory, logFactory, + messageFactory), numReconnectThreads); + } + + protected AbstractSocketInitiator(SessionSettings settings, SessionFactory sessionFactory, int numReconnectThreads) + throws ConfigError { super(settings, sessionFactory); IoBuffer.setAllocator(new SimpleBufferAllocator()); IoBuffer.setUseDirectBuffer(false); + if (numReconnectThreads > 0) { + scheduledReconnectExecutor = Executors.newScheduledThreadPool(numReconnectThreads, new QFScheduledReconnectThreadFactory()); + ((ThreadPoolExecutor) scheduledReconnectExecutor).setMaximumPoolSize(numReconnectThreads); + } else { + scheduledReconnectExecutor = null; + } } protected void createSessionInitiators() throws ConfigError { try { - createSessions(); + boolean continueInitOnError = isContinueInitOnError(); + createSessions(continueInitOnError); for (final Session session : getSessionMap().values()) { - createInitiator(session); + createInitiator(session, continueInitOnError); } } catch (final FieldConvertError e) { throw new ConfigError(e); } } - private void createInitiator(final Session session) throws ConfigError, FieldConvertError { + private void createInitiator(final Session session, final boolean continueInitOnError) throws ConfigError, FieldConvertError { SessionSettings settings = getSettings(); final SessionID sessionID = session.getSessionID(); @@ -145,14 +171,22 @@ && getSettings().isSetting(sessionID, Initiator.SETTING_PROXY_DOMAIN)) { proxyPort = (int) settings.getLong(sessionID, Initiator.SETTING_PROXY_PORT); } - final IoSessionInitiator ioSessionInitiator = new IoSessionInitiator(session, - socketAddresses, localAddress, reconnectingIntervals, - getScheduledExecutorService(), networkingOptions, - getEventHandlingStrategy(), getIoFilterChainBuilder(), sslEnabled, sslConfig, - proxyType, proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, proxyWorkstation); - - initiators.add(ioSessionInitiator); - + ScheduledExecutorService scheduledExecutorService = (scheduledReconnectExecutor != null ? scheduledReconnectExecutor : getScheduledExecutorService()); + try { + final IoSessionInitiator ioSessionInitiator = new IoSessionInitiator(session, + socketAddresses, localAddress, reconnectingIntervals, + scheduledExecutorService, settings, networkingOptions, + getEventHandlingStrategy(), getIoFilterChainBuilder(), sslEnabled, sslConfig, + proxyType, proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, proxyWorkstation); + + initiators.add(ioSessionInitiator); + } catch (ConfigError e) { + if (continueInitOnError) { + log.warn("error during session initialization for {}, continuing...", sessionID, e); + } else { + throw e; + } + } } // QFJ-482 @@ -175,13 +209,8 @@ private SocketAddress getLocalAddress(SessionSettings settings, final SessionID return localAddress; } - private void createSessions() throws ConfigError, FieldConvertError { + private void createSessions(boolean continueInitOnError) throws ConfigError, FieldConvertError { final SessionSettings settings = getSettings(); - boolean continueInitOnError = false; - if (settings.isSetting(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR)) { - continueInitOnError = settings.getBool(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR); - } - final Map initiatorSessions = new HashMap<>(); for (final Iterator i = settings.sectionIterator(); i.hasNext();) { final SessionID sessionID = i.next(); @@ -193,7 +222,7 @@ private void createSessions() throws ConfigError, FieldConvertError { } } catch (final Throwable e) { if (continueInitOnError) { - log.error("error during session initialization, continuing...", e); + log.warn("error during session initialization for {}, continuing...", sessionID, e); } else { throw e instanceof ConfigError ? (ConfigError) e : new ConfigError( "error during session initialization", e); @@ -209,7 +238,7 @@ public void createDynamicSession(SessionID sessionID) throws ConfigError { try { Session session = createSession(sessionID); super.addDynamicSession(session); - createInitiator(session); + createInitiator(session, isContinueInitOnError()); startInitiators(); } catch (final FieldConvertError e) { throw new ConfigError(e); @@ -309,4 +338,18 @@ public int getQueueSize() { } protected abstract EventHandlingStrategy getEventHandlingStrategy(); + + + private static class QFScheduledReconnectThreadFactory implements ThreadFactory { + + private static final AtomicInteger COUNTER = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, QFJ_RECONNECT_THREAD_PREFIX + COUNTER.getAndIncrement()); + thread.setDaemon(true); + return thread; + } + } + } diff --git a/quickfixj-core/src/main/java/quickfix/mina/initiator/ConnectException.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/ConnectException.java new file mode 100644 index 0000000000..91f949598c --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/ConnectException.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * 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 java.io.IOException; +import java.net.SocketAddress; + +public class ConnectException extends IOException { + + private final SocketAddress socketAddress; + + public ConnectException(SocketAddress socketAddress) { + this.socketAddress = socketAddress; + } + + public ConnectException(String message, Throwable cause, SocketAddress socketAddress) { + super(message, cause); + this.socketAddress = socketAddress; + } + + public ConnectException(String message, SocketAddress socketAddress) { + super(message); + this.socketAddress = socketAddress; + } + + public ConnectException(Throwable cause, SocketAddress socketAddress) { + super(cause.getMessage(), cause); + this.socketAddress = socketAddress; + } + + public SocketAddress getSocketAddress() { + return socketAddress; + } +} 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 437f4ff77d..95ede47167 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorIoHandler.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorIoHandler.java @@ -25,6 +25,7 @@ import quickfix.MessageUtils; import quickfix.Session; import quickfix.SessionID; +import quickfix.SessionSettings; import quickfix.field.ApplVerID; import quickfix.field.DefaultApplVerID; import quickfix.field.MsgType; @@ -40,9 +41,9 @@ class InitiatorIoHandler extends AbstractIoHandler { private final Session quickfixSession; private final EventHandlingStrategy eventHandlingStrategy; - public InitiatorIoHandler(Session quickfixSession, NetworkingOptions networkingOptions, - EventHandlingStrategy eventHandlingStrategy) { - super(networkingOptions, eventHandlingStrategy); + public InitiatorIoHandler(Session quickfixSession, SessionSettings sessionSettings, NetworkingOptions networkingOptions, + EventHandlingStrategy eventHandlingStrategy) { + super(sessionSettings, networkingOptions, eventHandlingStrategy); this.quickfixSession = quickfixSession; this.eventHandlingStrategy = eventHandlingStrategy; } diff --git a/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorProxyIoHandler.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorProxyIoHandler.java index 49bbdf620f..bdbc10f459 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorProxyIoHandler.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/InitiatorProxyIoHandler.java @@ -22,16 +22,12 @@ 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) { + InitiatorProxyIoHandler(InitiatorIoHandler initiatorIoHandler) { super(); this.initiatorIoHandler = initiatorIoHandler; - this.sslFilter = sslFilter; } @Override @@ -60,9 +56,6 @@ public void exceptionCaught(IoSession ioSession, Throwable cause) throws Excepti } @Override - public void proxySessionOpened(IoSession ioSession) throws Exception { - if (this.sslFilter != null) { - this.sslFilter.initiateHandshake(ioSession); - } + public void proxySessionOpened(IoSession 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 fa93ac6829..b3826b091f 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java @@ -24,11 +24,14 @@ import org.apache.mina.core.service.IoConnector; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.filter.ssl.SslFilter; import org.apache.mina.proxy.ProxyConnector; import org.apache.mina.transport.socket.SocketConnector; import quickfix.ConfigError; import quickfix.LogUtil; import quickfix.Session; +import quickfix.SessionID; +import quickfix.SessionSettings; import quickfix.SystemTime; import quickfix.mina.CompositeIoFilterChainBuilder; import quickfix.mina.EventHandlingStrategy; @@ -38,7 +41,6 @@ import quickfix.mina.message.FIXProtocolCodecFactory; import quickfix.mina.ssl.SSLConfig; import quickfix.mina.ssl.SSLContextFactory; -import quickfix.mina.ssl.SSLFilter; import quickfix.mina.ssl.SSLSupport; import javax.net.ssl.SSLContext; @@ -63,7 +65,7 @@ public class IoSessionInitiator { public IoSessionInitiator(Session fixSession, SocketAddress[] socketAddresses, SocketAddress localAddress, int[] reconnectIntervalInSeconds, - ScheduledExecutorService executor, NetworkingOptions networkingOptions, + ScheduledExecutorService executor, SessionSettings sessionSettings, NetworkingOptions networkingOptions, EventHandlingStrategy eventHandlingStrategy, IoFilterChainBuilder userIoFilterChainBuilder, boolean sslEnabled, SSLConfig sslConfig, String proxyType, String proxyVersion, String proxyHost, int proxyPort, @@ -76,7 +78,7 @@ public IoSessionInitiator(Session fixSession, SocketAddress[] socketAddresses, try { reconnectTask = new ConnectTask(sslEnabled, socketAddresses, localAddress, userIoFilterChainBuilder, fixSession, reconnectIntervalInMillis, - networkingOptions, eventHandlingStrategy, sslConfig, + sessionSettings, networkingOptions, eventHandlingStrategy, sslConfig, proxyType, proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, proxyWorkstation, log); } catch (GeneralSecurityException e) { throw new ConfigError(e); @@ -93,6 +95,7 @@ private static class ConnectTask implements Runnable { private IoConnector ioConnector; private final Session fixSession; private final long[] reconnectIntervalInMillis; + private final SessionSettings sessionSettings; private final NetworkingOptions networkingOptions; private final EventHandlingStrategy eventHandlingStrategy; private final SSLConfig sslConfig; @@ -117,7 +120,7 @@ private static class ConnectTask implements Runnable { public ConnectTask(boolean sslEnabled, SocketAddress[] socketAddresses, SocketAddress localAddress, IoFilterChainBuilder userIoFilterChainBuilder, Session fixSession, long[] reconnectIntervalInMillis, - NetworkingOptions networkingOptions, EventHandlingStrategy eventHandlingStrategy, SSLConfig sslConfig, + SessionSettings sessionSettings, 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 { @@ -127,6 +130,7 @@ public ConnectTask(boolean sslEnabled, SocketAddress[] socketAddresses, this.userIoFilterChainBuilder = userIoFilterChainBuilder; this.fixSession = fixSession; this.reconnectIntervalInMillis = reconnectIntervalInMillis; + this.sessionSettings = sessionSettings; this.networkingOptions = networkingOptions; this.eventHandlingStrategy = eventHandlingStrategy; this.sslConfig = sslConfig; @@ -149,16 +153,17 @@ private void setupIoConnector() throws ConfigError, GeneralSecurityException { boolean hasProxy = proxyType != null && proxyPort > 0 && socketAddresses[nextSocketAddressIndex] instanceof InetSocketAddress; - SSLFilter sslFilter = null; + SslFilter sslFilter = null; if (sslEnabled) { - sslFilter = installSslFilter(ioFilterChainBuilder, !hasProxy); + sslFilter = installSslFilter(ioFilterChainBuilder); } ioFilterChainBuilder.addLast(FIXProtocolCodecFactory.FILTER_NAME, new ProtocolCodecFilter(new FIXProtocolCodecFactory())); IoConnector newConnector; newConnector = ProtocolFactory.createIoConnector(socketAddresses[nextSocketAddressIndex]); - newConnector.setHandler(new InitiatorIoHandler(fixSession, networkingOptions, eventHandlingStrategy)); + networkingOptions.apply(newConnector); + newConnector.setHandler(new InitiatorIoHandler(fixSession, sessionSettings, networkingOptions, eventHandlingStrategy)); newConnector.setFilterChainBuilder(ioFilterChainBuilder); if (hasProxy) { @@ -170,9 +175,7 @@ private void setupIoConnector() throws ConfigError, GeneralSecurityException { ); proxyConnector.setHandler(new InitiatorProxyIoHandler( - new InitiatorIoHandler(fixSession, networkingOptions, eventHandlingStrategy), - sslFilter - )); + new InitiatorIoHandler(fixSession, sessionSettings, networkingOptions, eventHandlingStrategy))); newConnector = proxyConnector; } @@ -183,21 +186,21 @@ private void setupIoConnector() throws ConfigError, GeneralSecurityException { ioConnector = newConnector; } - private SSLFilter installSslFilter(CompositeIoFilterChainBuilder ioFilterChainBuilder, boolean autoStart) + private SslFilter installSslFilter(CompositeIoFilterChainBuilder ioFilterChainBuilder) throws GeneralSecurityException { final SSLContext sslContext = SSLContextFactory.getInstance(sslConfig); - final SSLFilter sslFilter = new SSLFilter(sslContext, autoStart); - sslFilter.setUseClientMode(true); - sslFilter.setCipherSuites(sslConfig.getEnabledCipherSuites() != null ? sslConfig.getEnabledCipherSuites() + final SslFilter sslFilter = new SslFilter(sslContext); + sslFilter.setEnabledCipherSuites(sslConfig.getEnabledCipherSuites() != null ? sslConfig.getEnabledCipherSuites() : SSLSupport.getDefaultCipherSuites(sslContext)); sslFilter.setEnabledProtocols(sslConfig.getEnabledProtocols() != null ? sslConfig.getEnabledProtocols() : SSLSupport.getSupportedProtocols(sslContext)); - sslFilter.setUseSNI(sslConfig.isUseSNI()); + sslFilter.setEndpointIdentificationAlgorithm(sslConfig.getEndpointIdentificationAlgorithm()); ioFilterChainBuilder.addLast(SSLSupport.FILTER_NAME, sslFilter); return sslFilter; } - public synchronized void run() { + @Override + public void run() { resetIoConnector(); try { if (connectFuture == null) { @@ -251,44 +254,33 @@ private void pollConnectFuture() { private void handleConnectException(Throwable e) { ++connectionFailureCount; SocketAddress socketAddress = socketAddresses[getCurrentSocketAddressIndex()]; - unresolveCurrentSocketAddress(socketAddress); while (e.getCause() != null) { e = e.getCause(); } final String nextRetryMsg = " (Next retry in " + computeNextRetryConnectDelay() + " milliseconds)"; + ConnectException wrappedException = new ConnectException(e, socketAddress); if (e instanceof IOException) { fixSession.getLog().onErrorEvent(e.getClass().getName() + " during connection to " + socketAddress + ": " + e + nextRetryMsg); } else { LogUtil.logThrowable(fixSession.getLog(), "Exception during connection to " + socketAddress + nextRetryMsg, e); } + fixSession.getStateListener().onConnectException(fixSession.getSessionID(), wrappedException); connectFuture = null; } private SocketAddress getNextSocketAddress() { SocketAddress socketAddress = socketAddresses[nextSocketAddressIndex]; - // QFJ-266 Recreate socket address for unresolved addresses + // Recreate socket address to avoid cached address resolution if (socketAddress instanceof InetSocketAddress) { InetSocketAddress inetAddr = (InetSocketAddress) socketAddress; - if (inetAddr.isUnresolved()) { - socketAddress = new InetSocketAddress(inetAddr.getHostName(), inetAddr - .getPort()); - socketAddresses[nextSocketAddressIndex] = socketAddress; - } + socketAddress = new InetSocketAddress(inetAddr.getHostName(), inetAddr.getPort()); + socketAddresses[nextSocketAddressIndex] = socketAddress; } nextSocketAddressIndex = (nextSocketAddressIndex + 1) % socketAddresses.length; return socketAddress; } - // QFJ-822 Reset cached DNS resolution information on connection failure. - private void unresolveCurrentSocketAddress(SocketAddress socketAddress) { - if (socketAddress instanceof InetSocketAddress) { - InetSocketAddress inetAddr = (InetSocketAddress) socketAddress; - socketAddresses[getCurrentSocketAddressIndex()] = InetSocketAddress.createUnresolved( - inetAddr.getHostString(), inetAddr.getPort()); - } - } - private int getCurrentSocketAddressIndex() { return (nextSocketAddressIndex + socketAddresses.length - 1) % socketAddresses.length; } @@ -372,4 +364,16 @@ synchronized void stop() { } SessionConnector.closeManagedSessionsAndDispose(reconnectTask.ioConnector, true, log); } + + public SessionID getSessionID() { + return reconnectTask.fixSession.getSessionID(); + } + + public SocketAddress getLocalAddress() { + return reconnectTask.localAddress; + } + + public SocketAddress[] getSocketAddresses() { + return Arrays.copyOf(reconnectTask.socketAddresses, reconnectTask.socketAddresses.length); + } } 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 9f6b6057a7..815b8b895d 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/message/FIXMessageDecoder.java +++ b/quickfixj-core/src/main/java/quickfix/mina/message/FIXMessageDecoder.java @@ -170,8 +170,8 @@ private boolean parseMessage(IoBuffer in, ProtocolDecoderOutput out) } else { if (position < in.limit()) { // if data remains String messageString = getMessageStringForError(in); - handleError(in, in.position() + 1, "Length format error in message (last character: " + (char)ch + "): " + messageString, - false); + handleError(in, position, "Length format error in message (last character: " + (char) ch + "): " + messageString, + false); continue; } else { break; diff --git a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLConfig.java b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLConfig.java index 667750f341..4d45fe9f8c 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLConfig.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLConfig.java @@ -20,6 +20,7 @@ package quickfix.mina.ssl; import java.util.Arrays; +import java.util.Objects; /** * Groups together SSL related configuration. @@ -36,58 +37,7 @@ public class SSLConfig { private String[] enabledProtocols; private String[] enabledCipherSuites; private boolean needClientAuth; - private boolean useSNI; - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - SSLConfig other = (SSLConfig) obj; - if (!Arrays.equals(enabledCipherSuites, other.enabledCipherSuites)) - return false; - if (!Arrays.equals(enabledProtocols, other.enabledProtocols)) - return false; - if (keyManagerFactoryAlgorithm == null) { - if (other.keyManagerFactoryAlgorithm != null) - return false; - } else if (!keyManagerFactoryAlgorithm.equals(other.keyManagerFactoryAlgorithm)) - return false; - if (keyStoreName == null) { - if (other.keyStoreName != null) - return false; - } else if (!keyStoreName.equals(other.keyStoreName)) - return false; - if (!Arrays.equals(keyStorePassword, other.keyStorePassword)) - return false; - if (keyStoreType == null) { - if (other.keyStoreType != null) - return false; - } else if (!keyStoreType.equals(other.keyStoreType)) - return false; - if (needClientAuth != other.needClientAuth) - return false; - if (trustManagerFactoryAlgorithm == null) { - if (other.trustManagerFactoryAlgorithm != null) - return false; - } else if (!trustManagerFactoryAlgorithm.equals(other.trustManagerFactoryAlgorithm)) - return false; - if (trustStoreName == null) { - if (other.trustStoreName != null) - return false; - } else if (!trustStoreName.equals(other.trustStoreName)) - return false; - if (!Arrays.equals(trustStorePassword, other.trustStorePassword)) - return false; - if(useSNI != other.useSNI) - return false; - if (trustStoreType == null) { - return other.trustStoreType == null; - } else return trustStoreType.equals(other.trustStoreType); - } + private String endpointIdentificationAlgorithm; public String[] getEnabledCipherSuites() { return enabledCipherSuites; @@ -129,31 +79,12 @@ public String getTrustStoreType() { return trustStoreType; } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(enabledCipherSuites); - result = prime * result + Arrays.hashCode(enabledProtocols); - result = prime * result + ((keyManagerFactoryAlgorithm == null) ? 0 : keyManagerFactoryAlgorithm.hashCode()); - result = prime * result + ((keyStoreName == null) ? 0 : keyStoreName.hashCode()); - result = prime * result + Arrays.hashCode(keyStorePassword); - result = prime * result + ((keyStoreType == null) ? 0 : keyStoreType.hashCode()); - result = prime * result + (needClientAuth ? 1231 : 1237); - result = prime * result - + ((trustManagerFactoryAlgorithm == null) ? 0 : trustManagerFactoryAlgorithm.hashCode()); - result = prime * result + ((trustStoreName == null) ? 0 : trustStoreName.hashCode()); - result = prime * result + Arrays.hashCode(trustStorePassword); - result = prime * result + ((trustStoreType == null) ? 0 : trustStoreType.hashCode()); - return result; - } - public boolean isNeedClientAuth() { return needClientAuth; } - public boolean isUseSNI() { - return useSNI; + public String getEndpointIdentificationAlgorithm() { + return endpointIdentificationAlgorithm; } public void setEnabledCipherSuites(String[] enabledCipherSuites) { @@ -184,8 +115,8 @@ public void setNeedClientAuth(boolean needClientAuth) { this.needClientAuth = needClientAuth; } - public void setUseSNI(boolean useSNI) { - this.useSNI = useSNI; + public void setEndpointIdentificationAlgorithm(String endpointIdentificationAlgorithm) { + this.endpointIdentificationAlgorithm = endpointIdentificationAlgorithm; } public void setTrustManagerFactoryAlgorithm(String trustManagerFactoryAlgorithm) { @@ -203,4 +134,33 @@ public void setTrustStorePassword(char[] trustStorePassword) { public void setTrustStoreType(String trustStoreType) { this.trustStoreType = trustStoreType; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SSLConfig sslConfig = (SSLConfig) o; + return needClientAuth == sslConfig.needClientAuth && + Objects.equals(keyStoreName, sslConfig.keyStoreName) && + Arrays.equals(keyStorePassword, sslConfig.keyStorePassword) && + Objects.equals(keyManagerFactoryAlgorithm, sslConfig.keyManagerFactoryAlgorithm) && + Objects.equals(keyStoreType, sslConfig.keyStoreType) && + Objects.equals(trustStoreName, sslConfig.trustStoreName) && + Arrays.equals(trustStorePassword, sslConfig.trustStorePassword) && + Objects.equals(trustManagerFactoryAlgorithm, sslConfig.trustManagerFactoryAlgorithm) && + Objects.equals(trustStoreType, sslConfig.trustStoreType) && + Arrays.equals(enabledProtocols, sslConfig.enabledProtocols) && + Arrays.equals(enabledCipherSuites, sslConfig.enabledCipherSuites) && + Objects.equals(endpointIdentificationAlgorithm, sslConfig.endpointIdentificationAlgorithm); + } + + @Override + public int hashCode() { + int result = Objects.hash(keyStoreName, keyManagerFactoryAlgorithm, keyStoreType, trustStoreName, trustManagerFactoryAlgorithm, trustStoreType, needClientAuth, endpointIdentificationAlgorithm); + result = 31 * result + Arrays.hashCode(keyStorePassword); + result = 31 * result + Arrays.hashCode(trustStorePassword); + result = 31 * result + Arrays.hashCode(enabledProtocols); + result = 31 * result + Arrays.hashCode(enabledCipherSuites); + return result; + } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLFilter.java b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLFilter.java deleted file mode 100644 index b9c036f9d2..0000000000 --- a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLFilter.java +++ /dev/null @@ -1,88 +0,0 @@ -/******************************************************************************* - * Copyright (c) quickfixengine.org All rights reserved. - * - * This file is part of the QuickFIX FIX Engine - * - * This file may be distributed under the terms of the quickfixengine.org - * license as defined by quickfixengine.org and appearing in the file - * LICENSE included in the packaging of this file. - * - * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING - * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE. - * - * See http://www.quickfixengine.org/LICENSE for licensing information. - * - * Contact ask@quickfixengine.org if any conditions of this licensing - * are not clear to you. - ******************************************************************************/ - -package quickfix.mina.ssl; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import javax.net.ssl.SSLContext; - -import javax.net.ssl.SSLException; -import org.apache.mina.core.filterchain.IoFilterChain; -import org.apache.mina.core.session.IoSession; -import org.apache.mina.filter.ssl.SslFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * An extended SSL filter based on MINA {@link SslFilter} that applies - * some adaptations. - */ -public class SSLFilter extends SslFilter { - - private final Logger log = LoggerFactory.getLogger(getClass()); - private boolean useSNI; - - public SSLFilter(SSLContext sslContext, boolean autoStart) { - super(sslContext, autoStart); - } - - public SSLFilter(SSLContext sslContext) { - super(sslContext); - } - - /** - * Called from {@link SslFilter#onPreAdd} every time a new - * session is created which makes it impossible to override enabled cipher - * suites configuration. - */ - @Override - public void setEnabledCipherSuites(String[] cipherSuites) { - } - - public void setCipherSuites(String[] cipherSuites) { - super.setEnabledCipherSuites(cipherSuites); - } - - /** - * Called before filter is added into the chain. - * We activate Server Name Indication if it is enabled in the session config. - */ - @Override - public void onPreAdd(IoFilterChain parent, String name, NextFilter nextFilter) - throws SSLException { - - if (useSNI) { - IoSession session = parent.getSession(); - SocketAddress remoteAddress = session.getRemoteAddress(); - - if (remoteAddress instanceof InetSocketAddress) { - // activate the SNI support in the JSSE SSLEngine - log.info("Activating TLS SNI support for peer address: {}", remoteAddress); - session.setAttribute(PEER_ADDRESS, remoteAddress); - } - } - - super.onPreAdd(parent, name, nextFilter); - } - - public void setUseSNI(boolean useSNI) { - this.useSNI = useSNI; - } -} 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 d821f2f64f..52550ad813 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLSupport.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLSupport.java @@ -39,7 +39,7 @@ public class SSLSupport { public static final String SETTING_TRUST_MANAGER_FACTORY_ALGORITHM = "TrustManagerFactoryAlgorithm"; public static final String SETTING_TRUST_STORE_TYPE = "TrustStoreType"; public static final String SETTING_NEED_CLIENT_AUTH = "NeedClientAuth"; - public static final String SETTING_USE_SNI = "UseSNI"; + public static final String SETTING_ENDPOINT_IDENTIFICATION_ALGORITHM = "EndpointIdentificationAlgorithm"; public static final String SETTING_ENABLED_PROTOCOLS = "EnabledProtocols"; public static final String SETTING_CIPHER_SUITES = "CipherSuites"; static final String DEFAULT_STORE_TYPE = "JKS"; @@ -111,7 +111,7 @@ public static SSLConfig getSslConfig(SessionSettings sessionSettings, SessionID sslConfig.setEnabledCipherSuites(getEnabledCipherSuites(sessionSettings, sessionID)); sslConfig.setEnabledProtocols(getEnabledProtocols(sessionSettings, sessionID)); sslConfig.setNeedClientAuth(isNeedClientAuth(sessionSettings, sessionID)); - sslConfig.setUseSNI(isUseSNI(sessionSettings, sessionID)); + sslConfig.setEndpointIdentificationAlgorithm(getEndpointIdentificationAlgorithm(sessionSettings, sessionID)); return sslConfig; } @@ -150,7 +150,7 @@ public static boolean isNeedClientAuth(SessionSettings sessionSettings, SessionI return "Y".equals(getString(sessionSettings, sessionID, SETTING_NEED_CLIENT_AUTH, "N")); } - public static boolean isUseSNI(SessionSettings sessionSettings, SessionID sessionID) { - return "Y".equals(getString(sessionSettings, sessionID, SETTING_USE_SNI, "N")); + public static String getEndpointIdentificationAlgorithm(SessionSettings sessionSettings, SessionID sessionID) { + return getString(sessionSettings, sessionID, SETTING_ENDPOINT_IDENTIFICATION_ALGORITHM, null); } } diff --git a/quickfixj-core/src/main/resources/config/sql/oracle/event_log_table.sql b/quickfixj-core/src/main/resources/config/sql/oracle/event_log_table.sql new file mode 100644 index 0000000000..1ac3d0cf3e --- /dev/null +++ b/quickfixj-core/src/main/resources/config/sql/oracle/event_log_table.sql @@ -0,0 +1,14 @@ +CREATE TABLE event_log ( + id INTEGER GENERATED ALWAYS AS IDENTITY INCREMENT BY 1 START WITH 1 CACHE 1000, + time TIMESTAMP NOT NULL, + beginstring VARCHAR2(8) NOT NULL, + sendercompid VARCHAR2(64) NOT NULL, + sendersubid VARCHAR2(64) NOT NULL, + senderlocid VARCHAR2(64) NOT NULL, + targetcompid VARCHAR2(64) NOT NULL, + targetsubid VARCHAR2(64) NOT NULL, + targetlocid VARCHAR2(64) NOT NULL, + session_qualifier VARCHAR2(64) NOT NULL, + text VARCHAR2(4000) NOT NULL, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/quickfixj-core/src/main/resources/config/sql/oracle/messages_log_table.sql b/quickfixj-core/src/main/resources/config/sql/oracle/messages_log_table.sql new file mode 100644 index 0000000000..7d05f7e639 --- /dev/null +++ b/quickfixj-core/src/main/resources/config/sql/oracle/messages_log_table.sql @@ -0,0 +1,14 @@ +CREATE TABLE messages_log ( + id INTEGER GENERATED ALWAYS AS IDENTITY INCREMENT BY 1 START WITH 1 CACHE 1000, + time TIMESTAMP NOT NULL, + beginstring VARCHAR2(8) NOT NULL, + sendercompid VARCHAR2(64) NOT NULL, + sendersubid VARCHAR2(64) NOT NULL, + senderlocid VARCHAR2(64) NOT NULL, + targetcompid VARCHAR2(64) NOT NULL, + targetsubid VARCHAR2(64) NOT NULL, + targetlocid VARCHAR2(64) NOT NULL, + session_qualifier VARCHAR2(64) NOT NULL, + text VARCHAR2(4000) NOT NULL, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/quickfixj-core/src/test/java/org/quickfixj/jmx/openmbean/CompositeTypeFactoryTest.java b/quickfixj-core/src/test/java/org/quickfixj/jmx/openmbean/CompositeTypeFactoryTest.java new file mode 100644 index 0000000000..55a82038ba --- /dev/null +++ b/quickfixj-core/src/test/java/org/quickfixj/jmx/openmbean/CompositeTypeFactoryTest.java @@ -0,0 +1,62 @@ +package org.quickfixj.jmx.openmbean; + +import org.junit.Before; +import org.junit.Test; + +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.SimpleType; + +import static org.junit.Assert.assertEquals; + +public class CompositeTypeFactoryTest { + + private CompositeTypeFactory underTest; + + @Before + public void setUp() { + underTest = new CompositeTypeFactory("user", "registered user"); + } + + @Test + public void shouldCreateCompositeTypeWithDescription() throws OpenDataException { + underTest.defineItem("age", "age of a person", SimpleType.INTEGER); + underTest.defineItem("email", "email address", SimpleType.STRING); + underTest.defineItem("dob", "date of birth", SimpleType.DATE); + + CompositeType compositeType = underTest.createCompositeType(); + assertEquals("user", compositeType.getTypeName()); + assertEquals("registered user", compositeType.getDescription()); + assertEquals("javax.management.openmbean.CompositeData", compositeType.getClassName()); + + assertEquals(SimpleType.INTEGER, compositeType.getType("age")); + assertEquals("age of a person", compositeType.getDescription("age")); + + assertEquals(SimpleType.STRING, compositeType.getType("email")); + assertEquals("email address", compositeType.getDescription("email")); + + assertEquals(SimpleType.DATE, compositeType.getType("dob")); + assertEquals("date of birth", compositeType.getDescription("dob")); + } + + @Test + public void shouldCreateCompositeTypeWithoutDescription() throws OpenDataException { + underTest.defineItem("age", SimpleType.INTEGER); + underTest.defineItem("email", SimpleType.STRING); + underTest.defineItem("dob", SimpleType.DATE); + + CompositeType compositeType = underTest.createCompositeType(); + assertEquals("user", compositeType.getTypeName()); + assertEquals("registered user", compositeType.getDescription()); + assertEquals("javax.management.openmbean.CompositeData", compositeType.getClassName()); + + assertEquals(SimpleType.INTEGER, compositeType.getType("age")); + assertEquals("age", compositeType.getDescription("age")); + + assertEquals(SimpleType.STRING, compositeType.getType("email")); + assertEquals("email", compositeType.getDescription("email")); + + assertEquals(SimpleType.DATE, compositeType.getType("dob")); + assertEquals("dob", compositeType.getDescription("dob")); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/AbstractMessageStoreTest.java b/quickfixj-core/src/test/java/quickfix/AbstractMessageStoreTest.java index f4a54a73a7..40632bc471 100644 --- a/quickfixj-core/src/test/java/quickfix/AbstractMessageStoreTest.java +++ b/quickfixj-core/src/test/java/quickfix/AbstractMessageStoreTest.java @@ -20,11 +20,14 @@ package quickfix; import junit.framework.TestCase; +import org.quickfixj.CharsetSupport; import java.io.IOException; import java.util.ArrayList; +import java.util.List; public abstract class AbstractMessageStoreTest extends TestCase { + private SessionID sessionID; private MessageStore store; @@ -130,7 +133,7 @@ public void testMessageStorageOutOfSequence() throws Exception { assertEquals("wrong message", "message2", messages.get(1)); } - public void testRefreshableMessageStore() throws Exception { + public void testRefreshMessageStore() throws Exception { if (!testEnabled) { return; } @@ -167,6 +170,44 @@ public void testRefreshableMessageStore() throws Exception { } } + public void testSetAndGetMessageWithAsciiCharacters() throws IOException { + MessageStore underTest = getStore(); + + if (underTest instanceof SleepycatStore) { + return; + } + + underTest.set(1, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVXYZ0123456789"); + + List messages = new ArrayList<>(); + underTest.get(1, 1, messages); + + assertEquals(1, messages.size()); + assertEquals("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVXYZ0123456789", messages.get(0)); + } + + public void testSetAndGetMessageWithUnicodeCharacters() throws IOException { + MessageStore underTest = getStore(); + + if (underTest instanceof SleepycatStore) { + return; + } + + CharsetSupport.setCharset("UTF-8"); + + try { + underTest.set(1, "a \u00A9 \u2603 \uD834\uDF06"); + + List messages = new ArrayList<>(); + underTest.get(1, 1, messages); + + assertEquals(1, messages.size()); + assertEquals("a \u00A9 \u2603 \uD834\uDF06", messages.get(0)); + } finally { + CharsetSupport.setDefaultCharset(); + } + } + protected void closeMessageStore(MessageStore store) throws IOException { // does nothing, by default } diff --git a/quickfixj-core/src/test/java/quickfix/ApplicationExtendedFunctionalAdapterTest.java b/quickfixj-core/src/test/java/quickfix/ApplicationExtendedFunctionalAdapterTest.java index 410f95981e..da4971bdfb 100644 --- a/quickfixj-core/src/test/java/quickfix/ApplicationExtendedFunctionalAdapterTest.java +++ b/quickfixj-core/src/test/java/quickfix/ApplicationExtendedFunctionalAdapterTest.java @@ -50,7 +50,7 @@ public void testRemovedCanLogonPredicateNotInvoked() { verify(predicate2).test(sessionID); verifyNoMoreInteractions(predicate2); - verifyZeroInteractions(predicate); + verifyNoInteractions(predicate); } @Test @@ -146,7 +146,7 @@ public void testRemovedOnBeforeSessionResetListenersNotInvoked() { adapter.removeBeforeSessionResetListener(listener); adapter.onBeforeSessionReset(sessionID); - verifyZeroInteractions(listener); + verifyNoInteractions(listener); } diff --git a/quickfixj-core/src/test/java/quickfix/ApplicationFunctionalAdapterTest.java b/quickfixj-core/src/test/java/quickfix/ApplicationFunctionalAdapterTest.java index 93bb22ca60..50d6c8ed06 100644 --- a/quickfixj-core/src/test/java/quickfix/ApplicationFunctionalAdapterTest.java +++ b/quickfixj-core/src/test/java/quickfix/ApplicationFunctionalAdapterTest.java @@ -44,7 +44,7 @@ public void testRemovedOnCreateListenersNotInvoked() { verify(listener2).accept(sessionID); verifyNoMoreInteractions(listener2); - verifyZeroInteractions(listener); + verifyNoInteractions(listener); } @Test @@ -80,7 +80,7 @@ public void testRemovedOnLogonListenersNotInvoked() { verify(listener2).accept(sessionID); verifyNoMoreInteractions(listener2); - verifyZeroInteractions(listener); + verifyNoInteractions(listener); } @Test @@ -116,7 +116,7 @@ public void testRemovedOnLogoutListenersNotInvoked() { verify(listener2).accept(sessionID); verifyNoMoreInteractions(listener2); - verifyZeroInteractions(listener); + verifyNoInteractions(listener); } @Test @@ -154,7 +154,7 @@ public void testRemovedToAdminListenersNotInvoked() { verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener2); - verifyZeroInteractions(listener); + verifyNoInteractions(listener); } @Test @@ -191,7 +191,7 @@ public void testToAdminTypeSafeListenersNotInvokedForUnmatchedMessageType() { verify(listener).accept(message, sessionID); verifyNoMoreInteractions(listener); - verifyZeroInteractions(listener2); + verifyNoInteractions(listener2); } @Test @@ -210,7 +210,7 @@ public void testRemovedToAdminTypeSafeListenersNotInvoked() { verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener2); - verifyZeroInteractions(listener); + verifyNoInteractions(listener); } @Test @@ -248,7 +248,7 @@ public void testRemovedFromAdminListenersNotInvoked() throws FieldNotFound, Inco verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener2); - verifyZeroInteractions(listener); + verifyNoInteractions(listener); } @Test @@ -296,7 +296,7 @@ private void assertFromAdminListenersFailFast(Exception exception) throws FieldN verify(listener).accept(message, sessionID); verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener, listener2); - verifyZeroInteractions(listener3); + verifyNoInteractions(listener3); } @Test @@ -333,7 +333,7 @@ public void testFromAdminTypeSafeListenersNotInvokedForUnmatchedMessageType() th verify(listener).accept(message, sessionID); verifyNoMoreInteractions(listener); - verifyZeroInteractions(listener2); + verifyNoInteractions(listener2); } @Test @@ -399,7 +399,7 @@ private void assertFromAdminTypeSafeListenersFailFast(Except verify(listener).accept(message, sessionID); verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener, listener2); - verifyZeroInteractions(listener3); + verifyNoInteractions(listener3); } @Test @@ -437,7 +437,7 @@ public void testRemovedToAppListenersNotInvoked() throws DoNotSend { verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener2); - verifyZeroInteractions(listener); + verifyNoInteractions(listener); } @Test @@ -467,7 +467,7 @@ public void testToAppListenersFailFastForDoNotSend() throws DoNotSend { verify(listener).accept(message, sessionID); verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener, listener2); - verifyZeroInteractions(listener3); + verifyNoInteractions(listener3); } @Test @@ -504,7 +504,7 @@ public void testToAppTypeSafeListenersNotInvokedForUnmatchedMessageType() throws verify(listener).accept(message, sessionID); verifyNoMoreInteractions(listener); - verifyZeroInteractions(listener2); + verifyNoInteractions(listener2); } @Test @@ -553,7 +553,7 @@ public void testToAppTypeSafeListenersFailFastForFieldNotFound() throws DoNotSen verify(listener).accept(message, sessionID); verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener, listener2); - verifyZeroInteractions(listener3); + verifyNoInteractions(listener3); } @Test @@ -591,7 +591,7 @@ public void testRemovedFromAppListenersNotInvoked() throws FieldNotFound, Incorr verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener2); - verifyZeroInteractions(listener); + verifyNoInteractions(listener); } @Test @@ -639,7 +639,7 @@ private void assertFromAppListenersFailFast(Exception exception) throws FieldNot verify(listener).accept(message, sessionID); verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener, listener2); - verifyZeroInteractions(listener3); + verifyNoInteractions(listener3); } @Test @@ -676,7 +676,7 @@ public void testFromAppTypeSafeListenersNotInvokedForUnmatchedMessageType() thro verify(listener).accept(message, sessionID); verifyNoMoreInteractions(listener); - verifyZeroInteractions(listener2); + verifyNoInteractions(listener2); } @Test @@ -742,7 +742,7 @@ private void assertFromAppTypeSafeListenersFailFast(Exceptio verify(listener).accept(message, sessionID); verify(listener2).accept(message, sessionID); verifyNoMoreInteractions(listener, listener2); - verifyZeroInteractions(listener3); + verifyNoInteractions(listener3); } private static class MyMessage1 extends Message { diff --git a/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java b/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java index 509c2b4f7b..add2d6fe1a 100644 --- a/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java +++ b/quickfixj-core/src/test/java/quickfix/DataDictionaryTest.java @@ -19,9 +19,24 @@ package quickfix; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasProperty; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + 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; @@ -32,7 +47,6 @@ import quickfix.field.LastMkt; import quickfix.field.MsgSeqNum; import quickfix.field.MsgType; -import quickfix.field.NoHops; import quickfix.field.NoPartyIDs; import quickfix.field.NoPartySubIDs; import quickfix.field.NoRelatedSym; @@ -59,690 +73,11 @@ import quickfix.fix44.QuoteRequest; import quickfix.test.util.ExpectedTestFailure; -import java.io.ByteArrayInputStream; -import java.math.BigDecimal; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.hasProperty; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - public class DataDictionaryTest { @Rule public final ExpectedException expectedException = ExpectedException.none(); - @Test - public void testDictionary() throws Exception { - DataDictionary dd = getDictionary(); - - assertEquals("wrong field name", "Currency", dd.getFieldName(15)); - assertEquals("wrong value description", "BUY", dd.getValueName(4, "B")); - assertEquals("wrong value type", FieldType.STRING, dd.getFieldType(1)); - assertEquals("wrong version", FixVersions.BEGINSTRING_FIX44, dd.getVersion()); - assertFalse("unexpected field values existence", dd.hasFieldValue(1)); - assertTrue("unexpected field values nonexistence", dd.hasFieldValue(4)); - assertFalse("unexpected field existence", dd.isField(9999)); - assertTrue("unexpected field nonexistence", dd.isField(4)); - assertTrue("unexpected field value existence", !dd.isFieldValue(4, "C")); - assertTrue("unexpected field value nonexistence", dd.isFieldValue(4, "B")); - assertTrue("wrong group info", dd.isGroup("A", 384)); - assertFalse("wrong group info", dd.isGroup("A", 1)); - assertNotNull("wrong group info", dd.getGroup("6", 232)); - assertTrue("incorrect header field", dd.isHeaderField(8)); - assertFalse("incorrect header field", dd.isHeaderField(1)); - assertTrue("incorrect trailer field", dd.isTrailerField(89)); - assertFalse("incorrect trailer field", dd.isTrailerField(1)); - assertTrue("incorrect message field", dd.isMsgField("A", 98)); - assertFalse("incorrect message field", dd.isMsgField("A", 1)); - // component field - assertTrue("incorrect message field", dd.isMsgField("6", 235)); - // group->component field - //assertTrue("incorrect message field", dd.isMsgField("6", 311)); - assertTrue("incorrect message type", dd.isMsgType("A")); - assertFalse("incorrect message type", dd.isMsgType("%")); - assertTrue("incorrect field requirement", dd.isRequiredField("A", 98)); - assertFalse("incorrect field requirement", dd.isRequiredField("A", 95)); - assertEquals("incorrect field name", "Account", dd.getFieldName(1)); - assertEquals("incorrect msg type", "0", dd.getMsgType("Heartbeat")); - assertEquals("incorrect msg type", "B", dd.getMsgType("News")); - assertFalse(dd.isMsgField("UNKNOWN_TYPE", 1)); - } - - @Test - public void testMissingFieldAttributeForRequired() throws Exception { - String data = ""; - data += ""; - data += "
"; - data += " "; - data += "
"; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += "
"; - - assertConfigErrorForMissingAttributeRequired(data); - } - - private void assertConfigErrorForMissingAttributeRequired(String data) { - try { - new DataDictionary(new ByteArrayInputStream(data.getBytes())); - } catch (ConfigError e) { - // Expected - assertTrue(e.getMessage().contains("does not have a 'required'")); - } - } - - @Test - public void testMissingComponentAttributeForRequired() throws Exception { - String data = ""; - data += ""; - data += "
"; - data += " "; - data += "
"; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += "
"; - - assertConfigErrorForMissingAttributeRequired(data); - } - - @Test - public void testMissingGroupAttributeForRequired() throws Exception { - String data = ""; - data += ""; - data += "
"; - data += " "; - data += "
"; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += "
"; - - assertConfigErrorForMissingAttributeRequired(data); - } - - @Test - public void testHeaderTrailerRequired() throws Exception { - String data = ""; - data += ""; - data += "
"; - data += " "; - data += " "; - data += "
"; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += " "; - data += "
"; - - DataDictionary dd = new DataDictionary(new ByteArrayInputStream(data.getBytes())); - assertEquals(1, dd.getNumMessageCategories()); - assertEquals("0", dd.getMsgType("Heartbeat")); - - assertTrue("BeginString should be required", dd.isRequiredHeaderField(8)); - assertFalse("OnBehalfOfCompID should not be required", dd.isRequiredHeaderField(115)); - assertTrue("Checksum should be required", dd.isRequiredTrailerField(10)); - assertFalse("Signature should not be required", dd.isRequiredTrailerField(89)); - - // now tests for fields that aren't actually in the dictionary - should come back false - assertFalse("Unknown header field shows up as required", dd.isRequiredHeaderField(666)); - 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(); - assertTrue(dd.isHeaderGroup(NoHops.FIELD)); - } - @Test public void testMessageValidateBodyOnly() throws Exception { final quickfix.fix44.NewOrderSingle newSingle = new quickfix.fix44.NewOrderSingle( @@ -752,7 +87,7 @@ public void testMessageValidateBodyOnly() throws Exception { newSingle.setField(new Price(42.37)); newSingle.setField(new HandlInst()); newSingle.setField(new Symbol("QFJ")); - newSingle.setField(new HandlInst(HandlInst.MANUAL_ORDER_BEST_EXECUTION)); + newSingle.setField(new HandlInst(HandlInst.MANUAL_ORDER)); newSingle.setField(new TimeInForce(TimeInForce.DAY)); newSingle.setField(new Account("testAccount")); @@ -770,7 +105,7 @@ protected void execute() throws Throwable { @Test public void testMessageDataDictionaryMismatch() throws Exception { final quickfix.fix43.NewOrderSingle newSingle = new quickfix.fix43.NewOrderSingle( - new ClOrdID("123"), new HandlInst(HandlInst.MANUAL_ORDER_BEST_EXECUTION), new Side(Side.BUY), new TransactTime(), new OrdType( + new ClOrdID("123"), new HandlInst(HandlInst.MANUAL_ORDER), new Side(Side.BUY), new TransactTime(), new OrdType( OrdType.LIMIT)); newSingle.setField(new OrderQty(42)); newSingle.setField(new Price(42.37)); @@ -791,50 +126,7 @@ protected void execute() throws Throwable { // If bodyOnly is true, the correct data dictionary is not checked. dd.validate(newSingle, true); } - - // QF C++ treats the string argument as a filename although it's - // named 'url'. QFJ string argument can be either but this test - // ensures the DD works correctly with a regular file path. - @Test - public void testDictionaryWithFilename() throws Exception { - DataDictionary dd = new DataDictionary("FIX40.xml"); - assertEquals("wrong field name", "Currency", dd.getFieldName(15)); - // It worked! - } - - // Support finding DD in classpath - @Test - public void testDictionaryInClassPath() throws Exception { - URLClassLoader customClassLoader = new URLClassLoader(new URL[] { new URL("file:etc") }, - getClass().getClassLoader()); - Thread currentThread = Thread.currentThread(); - ClassLoader previousContextClassLoader = currentThread.getContextClassLoader(); - currentThread.setContextClassLoader(customClassLoader); - try { - DataDictionary dd = new DataDictionary("FIX40.xml"); - assertEquals("wrong field name", "Currency", dd.getFieldName(15)); - // It worked! - } finally { - currentThread.setContextClassLoader(previousContextClassLoader); - } - } - - // QFJ-235 - @Test - public void testWildcardEnumValue() throws Exception { - DataDictionary dd = getDictionary(); - assertTrue(dd.isFieldValue(65, "FOO")); - } - - @Test - public void testMessageCategory() throws Exception { - DataDictionary dd = getDictionary(); - assertTrue(dd.isAdminMessage(MsgType.LOGON)); - assertFalse(dd.isAppMessage(MsgType.LOGON)); - assertFalse(dd.isAdminMessage(MsgType.ORDER_SINGLE)); - assertTrue(dd.isAppMessage(MsgType.ORDER_SINGLE)); - } - + @Test public void testAllowUnknownFields() throws Exception { final quickfix.fix44.NewOrderSingle newSingle = new quickfix.fix44.NewOrderSingle( @@ -850,7 +142,7 @@ public void testAllowUnknownFields() throws Exception { newSingle.setField(new Price(42.37)); newSingle.setField(new HandlInst()); newSingle.setField(new Symbol("QFJ")); - newSingle.setField(new HandlInst(HandlInst.MANUAL_ORDER_BEST_EXECUTION)); + newSingle.setField(new HandlInst(HandlInst.MANUAL_ORDER)); newSingle.setField(new TimeInForce(TimeInForce.DAY)); newSingle.setField(new Account("testAccount")); @@ -870,26 +162,6 @@ protected void execute() throws Throwable { dictionary.validate(newSingle); } - // QFJ-535 - @Test - public void testValidateFieldsOutOfOrderForGroups() throws Exception { - final DataDictionary dictionary = new DataDictionary(getDictionary()); - dictionary.setCheckUnorderedGroupFields(false); - Message messageWithGroupLevel1 = new Message( - "8=FIX.4.4\0019=185\00135=D\00134=25\00149=SENDER\00156=TARGET\00152=20110412-13:43:00\001" + - "60=20110412-13:43:00\0011=testAccount\00111=123\00121=3\00138=42\00140=2\00144=42.37\001" + - "54=1\00155=QFJ\00159=0\00178=1\00179=allocAccount\001736=currency\001661=1\00110=130\001", - dictionary); - dictionary.validate(messageWithGroupLevel1); - - Message messageWithGroupLevel2 = new Message( - "8=FIX.4.4\0019=185\00135=D\00134=25\00149=SENDER\00156=TARGET\00152=20110412-13:43:00\001" + - "60=20110412-13:43:00\0011=testAccount\00111=123\00121=3\00138=42\00140=2\00144=42.37\001" + - "54=1\00155=QFJ\00159=0\00178=1\00179=allocAccount\001539=1\001524=1\001538=1\001525=a\00110=145\001", - dictionary); - dictionary.validate(messageWithGroupLevel2); - } - // QFJ-535 @Test public void testNewOrderSingleWithCorrectTag50() throws Exception { @@ -1344,7 +616,7 @@ public void testAllowingBlankValuesDisablesFieldValidation() throws Exception { newSingle.setField(new Price(42.37)); newSingle.setField(new HandlInst()); newSingle.setField(new Symbol("QFJ")); - newSingle.setField(new HandlInst(HandlInst.MANUAL_ORDER_BEST_EXECUTION)); + newSingle.setField(new HandlInst(HandlInst.MANUAL_ORDER)); newSingle.setField(new TimeInForce(TimeInForce.DAY)); newSingle.setField(new Account("testAccount")); newSingle.setField(new StringField(EffectiveTime.FIELD)); @@ -1376,7 +648,7 @@ public void testConcurrentValidationFailure() throws Exception { Message msg = MessageUtils.parse(messageFactory, dd, msgString); Group partyGroup = msg.getGroups(quickfix.field.NoPartyIDs.FIELD).get(0); char partyIdSource = partyGroup.getChar(PartyIDSource.FIELD); - assertEquals(PartyIDSource.PROPRIETARY_CUSTOM_CODE, partyIdSource); + assertEquals(PartyIDSource.PROPRIETARY, partyIdSource); return msg; }; resultList.add(ptpe.submit(messageParser)); @@ -1395,8 +667,6 @@ public void testConcurrentValidationFailure() throws Exception { } } - - // // Group Validation Tests in RepeatingGroupTest // @@ -1432,4 +702,5 @@ public static DataDictionary getDictionary(String fileName) throws Exception { return new DataDictionary(DataDictionaryTest.class.getClassLoader() .getResourceAsStream(fileName)); } + } diff --git a/quickfixj-core/src/test/java/quickfix/DefaultDataDictionaryProviderTest.java b/quickfixj-core/src/test/java/quickfix/DefaultDataDictionaryProviderTest.java index c0ada09867..cbe17aa69b 100644 --- a/quickfixj-core/src/test/java/quickfix/DefaultDataDictionaryProviderTest.java +++ b/quickfixj-core/src/test/java/quickfix/DefaultDataDictionaryProviderTest.java @@ -19,8 +19,14 @@ package quickfix; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.hamcrest.Matchers.equalTo; import static quickfix.field.ApplVerID.*; import org.junit.BeforeClass; @@ -47,7 +53,7 @@ public void returnRegisteredSessonDictionaryWithoutDiscovery() throws Exception DataDictionary dd = provider.getSessionDataDictionary(FixVersions.BEGINSTRING_FIX44); - assertThat(dd, is(dictionaryForTest2)); + assertThat(dd, sameInstance(dictionaryForTest2)); } @Test @@ -57,7 +63,7 @@ public void returnNullSessonDictionaryWithoutDiscovery() throws Exception { DataDictionary dd = provider.getSessionDataDictionary(FixVersions.BEGINSTRING_FIX44); - assertThat(dd, is(nullValue())); + assertThat(dd, nullValue()); } @Test @@ -66,8 +72,8 @@ public void returnSessionDictionaryWithDiscovery() throws Exception { DataDictionary dd = provider.getSessionDataDictionary(FixVersions.BEGINSTRING_FIX40); - assertThat(dd, is(notNullValue())); - assertThat(dd.getVersion(), is(FixVersions.BEGINSTRING_FIX40)); + assertThat(dd, notNullValue()); + assertThat(dd.getVersion(), equalTo(FixVersions.BEGINSTRING_FIX40)); } @Test @@ -77,7 +83,7 @@ public void throwExceptionIfSessionDictionaryIsNotFound() throws Exception { try { provider.getSessionDataDictionary("FIX44_Invalid_Test"); } catch (QFJException e) { - assertThat(e.getCause(), is(ConfigError.class)); + assertThat(e.getCause(), instanceOf(ConfigError.class)); } } @@ -89,7 +95,7 @@ public void returnRegisteredAppDictionaryWithoutDiscovery() throws Exception { DataDictionary dd = provider.getApplicationDataDictionary(new ApplVerID(FIX40)); - assertThat(dd, is(dictionaryForTest2)); + assertThat(dd, sameInstance(dictionaryForTest2)); } @Test @@ -99,7 +105,7 @@ public void returnNullAppDictionaryWithoutDiscovery() throws Exception { DataDictionary dd = provider.getApplicationDataDictionary(new ApplVerID(FIX40)); - assertThat(dd, is(nullValue())); + assertThat(dd, nullValue()); } @Test @@ -109,8 +115,8 @@ public void returnAppDictionaryWithDiscovery() throws Exception { DataDictionary dd = provider.getApplicationDataDictionary(new ApplVerID(FIX40)); - assertThat(dd, is(notNullValue())); - assertThat(dd.getVersion(), is(FixVersions.BEGINSTRING_FIX40)); + assertThat(dd, notNullValue()); + assertThat(dd.getVersion(), equalTo(FixVersions.BEGINSTRING_FIX40)); } @Test diff --git a/quickfixj-core/src/test/java/quickfix/DefaultMessageFactoryTest.java b/quickfixj-core/src/test/java/quickfix/DefaultMessageFactoryTest.java index 1fec31694c..4803934bb6 100644 --- a/quickfixj-core/src/test/java/quickfix/DefaultMessageFactoryTest.java +++ b/quickfixj-core/src/test/java/quickfix/DefaultMessageFactoryTest.java @@ -41,6 +41,7 @@ public void testMessageCreate() throws Exception { 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.fixlatest.Advertisement.class, MsgType.ADVERTISEMENT, factory.create(FixVersions.FIXLATEST, MsgType.ADVERTISEMENT)); assertMessage(quickfix.Message.class, MsgType.ADVERTISEMENT, factory.create("unknown", MsgType.ADVERTISEMENT)); } @@ -56,6 +57,7 @@ public void testFixtCreate() throws Exception { 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)); + assertMessage(quickfix.fixlatest.Email.class, MsgType.EMAIL, factory.create(BEGINSTRING_FIXT11, new ApplVerID(ApplVerID.FIXLATEST), MsgType.EMAIL)); } @Test @@ -74,6 +76,7 @@ protected void execute() throws Throwable { 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()); + assertEquals(quickfix.fixlatest.News.NoLinesOfText.class, factory.create(FixVersions.FIXLATEST, 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)); } @@ -93,7 +96,8 @@ public static Object[][] getParameters() { {ApplVerID.FIX44, quickfix.fix44.Email.class}, {ApplVerID.FIX50, quickfix.fix50.Email.class}, {ApplVerID.FIX50SP1, quickfix.fix50sp1.Email.class}, - {ApplVerID.FIX50SP2, quickfix.fix50sp2.Email.class} + {ApplVerID.FIX50SP2, quickfix.fix50sp2.Email.class}, + {ApplVerID.FIXLATEST, quickfix.fixlatest.Email.class} }; } } diff --git a/quickfixj-core/src/test/java/quickfix/ExceptionTest.java b/quickfixj-core/src/test/java/quickfix/ExceptionTest.java index 7da22e93f5..13572eb49f 100644 --- a/quickfixj-core/src/test/java/quickfix/ExceptionTest.java +++ b/quickfixj-core/src/test/java/quickfix/ExceptionTest.java @@ -23,36 +23,15 @@ public class ExceptionTest extends TestCase { - public void testDoNotSend() { - new DoNotSend(); - } - - public void testIncorrectDataFormat() { - IncorrectDataFormat e = new IncorrectDataFormat(5, "test"); - assertEquals(5, e.getField()); - assertEquals("test", e.getData()); - } - - public void testIncorrectTagValue() { - new IncorrectTagValue(5); - IncorrectTagValue e = new IncorrectTagValue(5, "test"); - } - public void testRejectLogon() { new RejectLogon(); } - - public void testRuntimeError() { - new RuntimeError(); - new RuntimeError("test"); - new RuntimeError(new Exception()); - } - + public void testSessionNotFound() { new SessionNotFound(); new SessionNotFound("test"); } - + public void testSessionException() { new SessionException(); new SessionException("test"); diff --git a/quickfixj-core/src/test/java/quickfix/FieldMapTest.java b/quickfixj-core/src/test/java/quickfix/FieldMapTest.java index 9eb376a5c0..beac5e6eeb 100644 --- a/quickfixj-core/src/test/java/quickfix/FieldMapTest.java +++ b/quickfixj-core/src/test/java/quickfix/FieldMapTest.java @@ -1,35 +1,26 @@ package quickfix; -import java.math.BigDecimal; +import static org.junit.Assert.assertEquals; + import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneOffset; -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; + +import org.junit.Test; + import quickfix.field.EffectiveTime; import quickfix.field.MDEntryTime; import quickfix.field.converter.UtcTimeOnlyConverter; -import java.util.Iterator; -import java.util.Optional; - /** * Tests the {@link FieldMap} class. * Specifically, verifies that the setters for {@link UtcTimeStampField} work correctly. * * @author toli - * @version $Id$ */ -public class FieldMapTest extends TestCase { - public FieldMapTest(String inName) { - super(inName); - } - - public static Test suite() { - return new TestSuite(FieldMapTest.class); - } +public class FieldMapTest { + @Test public void testSetUtcTimeStampField() throws Exception { FieldMap map = new Message(); LocalDateTime aDate = LocalDateTime.now(); @@ -43,6 +34,7 @@ public void testSetUtcTimeStampField() throws Exception { epochMilliOfLocalDate(map.getField(new EffectiveTime()).getValue())); } + @Test public void testSetUtcTimeOnlyField() throws Exception { FieldMap map = new Message(); LocalTime aDate = LocalTime.now(); @@ -59,6 +51,7 @@ public void testSetUtcTimeOnlyField() throws Exception { /** * Try a subclass of {@link UtcTimeOnlyField} and {@link UtcTimeStampField} directly */ + @Test public void testSpecificFields() throws Exception { FieldMap map = new Message(); LocalDateTime aDate = LocalDateTime.now(); @@ -71,46 +64,6 @@ public void testSpecificFields() throws Exception { UtcTimeOnlyConverter.convert(map.getField(new MDEntryTime()).getValue(), UtcTimestampPrecision.MILLIS)); } - private void testOrdering(int[] vals, int[] order, int[] expected) { - FieldMap map = new Message(order); - for (int v : vals) - map.setInt(v, v); - Iterator> it = map.iterator(); - for (int e : expected) - assertEquals(String.valueOf(e), it.next().getObject()); - } - - public void testOrdering() { - testOrdering(new int[] { 1, 2, 3 }, null, new int[] { 1, 2, 3 }); - testOrdering(new int[] { 3, 2, 1 }, null, new int[] { 1, 2, 3 }); - testOrdering(new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 }); - testOrdering(new int[] { 3, 2, 1 }, new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 }); - testOrdering(new int[] { 1, 2, 3 }, new int[] { 1, 3, 2 }, new int[] { 1, 3, 2 }); - testOrdering(new int[] { 3, 2, 1 }, new int[] { 1, 3, 2 }, new int[] { 1, 3, 2 }); - testOrdering(new int[] { 1, 2, 3 }, new int[] { 1, 3 }, new int[] { 1, 3, 2 }); - testOrdering(new int[] { 3, 2, 1 }, new int[] { 1, 3 }, new int[] { 1, 3, 2 }); - testOrdering(new int[] { 1, 2, 3 }, new int[] { 3, 1 }, new int[] { 3, 1, 2 }); - testOrdering(new int[] { 3, 2, 1 }, new int[] { 3, 1 }, new int[] { 3, 1, 2 }); - } - - 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()); - } - - public void testOptionalDecimal() { - FieldMap map = new Message(); - map.setField(new DecimalField(44, new BigDecimal("1565.10"))); - Optional optValue = map.getOptionalDecimal(44); - assertTrue(optValue.isPresent()); - assertEquals(0, optValue.get().compareTo(new BigDecimal("1565.10"))); - assertFalse(map.getOptionalDecimal(6).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 cce1cc4048..879ade68fb 100644 --- a/quickfixj-core/src/test/java/quickfix/FieldTest.java +++ b/quickfixj-core/src/test/java/quickfix/FieldTest.java @@ -19,8 +19,17 @@ package quickfix; +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 org.junit.Test; import org.quickfixj.CharsetSupport; + import quickfix.field.ClOrdID; import quickfix.field.ExecInst; import quickfix.field.MDUpdateAction; @@ -32,14 +41,6 @@ import quickfix.fix50.MarketDataIncrementalRefresh; import quickfix.fix50.NewOrderSingle; -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 org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -89,7 +90,7 @@ public void testFieldCalculationsWithUTF8Charset() throws UnsupportedEncodingExc try { testFieldCalcuations("\u6D4B\u9A8C\u6570\u636E", 50, 16); } finally { - CharsetSupport.setCharset(CharsetSupport.getDefaultCharset()); + CharsetSupport.setDefaultCharset(); } } @@ -251,16 +252,22 @@ public void testBytesField() { } @Test - public void testFieldhashCode() throws Exception { - assertEqualsAndHash(new IntField(11, 100), new IntField(11, 100)); - assertEqualsAndHash(new DoubleField(11, 100.0), new DoubleField(11, 100.0)); - 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')); - 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)); + public void testBytesFieldFullRange() { + byte[] data = new byte[256]; + + for (int i = 0; i < 256; i++) { + data[i] = (byte) i; + } + + BytesField field = new BytesField(RawData.FIELD); + field.setValue(data); + + byte[] tagPrefixedData = field.toString().getBytes(CharsetSupport.getCharsetInstance()); + assertEquals(256 + 3, tagPrefixedData.length); + + for (int i = 0; i < data.length; ++i) { + assertEquals(data[i], tagPrefixedData[i + 3]); + } } // QFJ-881 diff --git a/quickfixj-core/src/test/java/quickfix/FileStoreTest.java b/quickfixj-core/src/test/java/quickfix/FileStoreTest.java index 8612368964..08b09c25dd 100644 --- a/quickfixj-core/src/test/java/quickfix/FileStoreTest.java +++ b/quickfixj-core/src/test/java/quickfix/FileStoreTest.java @@ -19,6 +19,8 @@ package quickfix; +import org.quickfixj.CharsetSupport; + import java.io.IOException; import java.util.ArrayList; import java.util.Date; @@ -26,8 +28,9 @@ public class FileStoreTest extends AbstractMessageStoreTest { - protected void tearDown() throws Exception { + public void tearDown() throws Exception { super.tearDown(); + CharsetSupport.setDefaultCharset(); FileStore fileStore = (FileStore) getStore(); try { fileStore.closeAndDeleteFiles(); @@ -36,6 +39,7 @@ protected void tearDown() throws Exception { } } + @Override protected MessageStoreFactory getMessageStoreFactory() throws ConfigError, FieldConvertError { SessionSettings settings = new SessionSettings(getConfigurationFileName()); // Initialize the session settings from the defaults @@ -44,6 +48,7 @@ protected MessageStoreFactory getMessageStoreFactory() throws ConfigError, Field return new FileStoreFactory(settings); } + @Override protected Class getMessageStoreClass() { return FileStore.class; } diff --git a/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java b/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java index 9c1d5147b2..189f9e1449 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcLogTest.java @@ -127,16 +127,17 @@ private void dropTable(String tableName) throws SQLException { private void setUpJdbcLog(boolean filterHeartbeats, DataSource dataSource) throws ClassNotFoundException, SQLException, ConfigError { connection = JdbcTestSupport.getConnection(); + long now = System.currentTimeMillis(); + sessionID = new SessionID("FIX.4.2", "SENDER-" + now, "TARGET-" + now); SessionSettings settings = new SessionSettings(); if (filterHeartbeats) { settings.setBool(JdbcSetting.SETTING_JDBC_LOG_HEARTBEATS, false); } + settings.setString(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_TEST_QUERY, "SELECT COUNT(1) FROM INFORMATION_SCHEMA.SYSTEM_USERS WHERE 1 = 0;"); JdbcTestSupport.setHypersonicSettings(settings); initializeTableDefinitions(connection); logFactory = new JdbcLogFactory(settings); logFactory.setDataSource(dataSource); - long now = System.currentTimeMillis(); - sessionID = new SessionID("FIX.4.2", "SENDER-" + now, "TARGET-" + now); settings.setString(sessionID, "ConnectionType", "acceptor"); log = (JdbcLog) logFactory.create(sessionID); assertEquals(0, getRowCount(connection, log.getIncomingMessagesTableName())); diff --git a/quickfixj-core/src/test/java/quickfix/JdbcStoreLegacyTest.java b/quickfixj-core/src/test/java/quickfix/JdbcStoreLegacyTest.java index 49e1622787..fc38ce7c5d 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcStoreLegacyTest.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcStoreLegacyTest.java @@ -33,7 +33,7 @@ protected void initializeTableDefinitions(String sessionsTableName, String messa throws ConfigError, SQLException, IOException { Connection connection = null; try { - connection = getDataSource().getConnection(); + connection = getTestDataSource().getConnection(); JdbcTestSupport.loadSQL(connection, "config/sql/hsqldb/messages_table.sql", new JdbcTestSupport.HypersonicLegacyPreprocessor(messagesTableName)); diff --git a/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java b/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java index 647e595224..b3fe5f93cb 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcStoreTest.java @@ -54,7 +54,7 @@ protected void setUp() throws Exception { } protected void tearDown() throws Exception { - assertNoActiveConnections(); + assertNoActiveConnections(getTestDataSource()); if (initialContextFactory != null) { System.setProperty(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); } @@ -62,7 +62,7 @@ protected void tearDown() throws Exception { } private void bindDataSource() throws NamingException { - new InitialContext().rebind("TestDataSource", getDataSource()); + new InitialContext().rebind("TestDataSource", getTestDataSource()); } protected MessageStoreFactory getMessageStoreFactory() throws ConfigError, SQLException, @@ -91,7 +91,7 @@ private JdbcStoreFactory getMessageStoreFactory(String sessionTableName, String public void testExplicitDataSource() throws Exception { // No JNDI data source name is set up here JdbcStoreFactory factory = new JdbcStoreFactory(new SessionSettings()); - factory.setDataSource(getDataSource()); + factory.setDataSource(getTestDataSource()); factory.create(new SessionID("FIX4.4", "SENDER", "TARGET")); } @@ -124,7 +124,7 @@ protected void initializeTableDefinitions(String sessionsTableName, String messa throws ConfigError, SQLException, IOException { Connection connection = null; try { - connection = getDataSource().getConnection(); + connection = getTestDataSource().getConnection(); if (messagesTableName != null) { dropTable(connection, messagesTableName); } @@ -140,8 +140,8 @@ protected void initializeTableDefinitions(String sessionsTableName, String messa } } - protected DataSource getDataSource() { - return JdbcUtil.getDataSource(HSQL_DRIVER, HSQL_CONNECTION_URL, HSQL_USER, "", true); + protected DataSource getTestDataSource() { + return JdbcTestSupport.getTestDataSource(HSQL_DRIVER, HSQL_CONNECTION_URL, HSQL_USER, ""); } public void testCreationTime() throws Exception { diff --git a/quickfixj-core/src/test/java/quickfix/JdbcTestSupport.java b/quickfixj-core/src/test/java/quickfix/JdbcTestSupport.java index a21357af25..a2f573f472 100644 --- a/quickfixj-core/src/test/java/quickfix/JdbcTestSupport.java +++ b/quickfixj-core/src/test/java/quickfix/JdbcTestSupport.java @@ -19,6 +19,9 @@ package quickfix; +import com.zaxxer.hikari.HikariDataSource; + +import javax.sql.DataSource; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; @@ -26,13 +29,11 @@ import java.sql.SQLException; import java.sql.Statement; -import org.junit.Assert; - -import org.logicalcobwebs.proxool.ProxoolException; -import org.logicalcobwebs.proxool.ProxoolFacade; -import org.logicalcobwebs.proxool.admin.SnapshotIF; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class JdbcTestSupport { + public static final String HSQL_DRIVER = "org.hsqldb.jdbcDriver"; public static final String HSQL_CONNECTION_URL = "jdbc:hsqldb:mem:quickfixj"; public static final String HSQL_USER = "sa"; @@ -102,7 +103,7 @@ public static void dropTable(Connection connection, String tableName) throws SQL execSQL(connection, "drop table " + tableName + " if exists"); } - public static void execSQL(Connection connection, String sql) throws SQLException, IOException { + public static void execSQL(Connection connection, String sql) throws SQLException { Statement stmt = connection.createStatement(); stmt.execute(sql); stmt.close(); @@ -115,12 +116,24 @@ private static String getString(InputStream in) throws IOException { return new String(b); } - static void assertNoActiveConnections() throws ProxoolException { - for (String alias : ProxoolFacade.getAliases()) { - SnapshotIF snapshot = ProxoolFacade.getSnapshot(alias, true); - Assert.assertEquals("unclosed connections: " + alias, 0, snapshot - .getActiveConnectionCount()); - } + static void assertNoActiveConnections(DataSource dataSource) { + assertTrue(dataSource instanceof HikariDataSource); + + HikariDataSource hikariDataSource = (HikariDataSource) dataSource; + assertEquals("Some connections are still alive", 0, hikariDataSource.getHikariPoolMXBean().getActiveConnections()); } + static DataSource getTestDataSource(String jdbcDriver, String connectionURL, String user, String password) { + SessionID sessionID = new SessionID("TEST", "", ""); + + SessionSettings settings = new SessionSettings(); + // HSQL doesn't support JDBC4 which means that test query has to be supplied to HikariCP + settings.setString(sessionID, JdbcSetting.SETTING_JDBC_CONNECTION_TEST_QUERY, "SELECT COUNT(1) FROM INFORMATION_SCHEMA.SYSTEM_USERS WHERE 1 = 0;"); + + try { + return JdbcUtil.getOrCreatePooledDataSource(settings, sessionID, jdbcDriver, connectionURL, user, password); + } catch (ConfigError | FieldConvertError e) { + throw new RuntimeException("Unable to get or create pooled data source", e); + } + } } diff --git a/quickfixj-core/src/test/java/quickfix/MessageComponentTest.java b/quickfixj-core/src/test/java/quickfix/MessageComponentTest.java new file mode 100644 index 0000000000..cea7776dc0 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/MessageComponentTest.java @@ -0,0 +1,162 @@ +package quickfix; + +import org.junit.Test; +import quickfix.field.AgreementCurrency; +import quickfix.field.Product; +import quickfix.field.SecurityType; +import quickfix.field.Symbol; +import quickfix.fix44.QuoteRequest; +import quickfix.fix44.component.FinancingDetails; +import quickfix.fix44.component.Instrument; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class MessageComponentTest { + + @Test + public void shouldCopyCustomTagsToComponent() throws FieldNotFound { + Instrument instrument1 = new Instrument(); + instrument1.set(new Symbol("EURUSD")); + instrument1.set(new Product(Product.CURRENCY)); + instrument1.set(new SecurityType(SecurityType.FOREIGN_EXCHANGE_CONTRACT)); + instrument1.setString(12345, "ABC"); + instrument1.setInt(54321, 0xCAFE); + + assertEquals("EURUSD", instrument1.getSymbol().getValue()); + assertEquals(Product.CURRENCY, instrument1.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, instrument1.getSecurityType().getValue()); + assertEquals("ABC", instrument1.getString(12345)); + assertEquals(0xCAFE, instrument1.getInt(54321)); + + Instrument instrument2 = new Instrument(); + instrument1.copyTo(instrument2); + + assertEquals("EURUSD", instrument2.getSymbol().getValue()); + assertEquals(Product.CURRENCY, instrument2.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, instrument2.getSecurityType().getValue()); + + assertEquals("ABC", instrument2.getString(12345)); + assertEquals(0xCAFE, instrument2.getInt(54321)); + } + + @Test + public void shouldNotCopyCustomTagsFromComponent() throws FieldNotFound { + Instrument instrument1 = new Instrument(); + instrument1.set(new Symbol("EURUSD")); + instrument1.set(new Product(Product.CURRENCY)); + instrument1.set(new SecurityType(SecurityType.FOREIGN_EXCHANGE_CONTRACT)); + instrument1.setString(12345, "ABC"); + instrument1.setInt(54321, 0xCAFE); + + assertEquals("EURUSD", instrument1.getSymbol().getValue()); + assertEquals(Product.CURRENCY, instrument1.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, instrument1.getSecurityType().getValue()); + assertEquals("ABC", instrument1.getString(12345)); + assertEquals(0xCAFE, instrument1.getInt(54321)); + + Instrument instrument2 = new Instrument(); + instrument2.copyFrom(instrument1); + + assertEquals("EURUSD", instrument2.getSymbol().getValue()); + assertEquals(Product.CURRENCY, instrument2.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, instrument2.getSecurityType().getValue()); + + assertFalse(instrument2.isSetField(12345)); + assertFalse(instrument2.isSetField(54321)); + } + + @Test + public void shouldSetComponentWithCustomTags() throws FieldNotFound { + Instrument instrument = new Instrument(); + instrument.set(new Symbol("EURUSD")); + instrument.set(new Product(Product.CURRENCY)); + instrument.set(new SecurityType(SecurityType.FOREIGN_EXCHANGE_CONTRACT)); + instrument.setString(12345, "ABC"); + instrument.setInt(54321, 0xCAFE); + + FinancingDetails financingDetails = new FinancingDetails(); + financingDetails.set(new AgreementCurrency("USD")); + financingDetails.setString(111222, "DEF"); + + QuoteRequest.NoRelatedSym noRelatedSym = new QuoteRequest.NoRelatedSym(); + noRelatedSym.set(instrument); + noRelatedSym.set(financingDetails); + + assertEquals("EURUSD", noRelatedSym.getSymbol().getValue()); + assertEquals(Product.CURRENCY, noRelatedSym.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, noRelatedSym.getSecurityType().getValue()); + assertEquals("ABC", noRelatedSym.getString(12345)); + assertEquals(0xCAFE, noRelatedSym.getInt(54321)); + + assertEquals("USD", noRelatedSym.getFinancingDetails().getAgreementCurrency().getValue()); + assertEquals("DEF", noRelatedSym.getString(111222)); + } + + @Test + public void shouldOverrideCustomComponentTags() throws FieldNotFound { + Instrument instrument = new Instrument(); + instrument.set(new Symbol("EURUSD")); + instrument.set(new Product(Product.CURRENCY)); + instrument.set(new SecurityType(SecurityType.FOREIGN_EXCHANGE_CONTRACT)); + instrument.setString(12345, "ABC"); + instrument.setInt(54321, 0xCAFE); + + FinancingDetails financingDetails = new FinancingDetails(); + financingDetails.set(new AgreementCurrency("USD")); + financingDetails.setString(111222, "DEF"); + + QuoteRequest.NoRelatedSym noRelatedSym = new QuoteRequest.NoRelatedSym(); + noRelatedSym.set(instrument); + noRelatedSym.set(financingDetails); + + instrument.set(new Symbol("USDCAD")); + instrument.setString(12345, "XYZ"); + noRelatedSym.set(instrument); + + financingDetails.set(new AgreementCurrency("CAD")); + financingDetails.setString(111222, "GHI"); + financingDetails.setInt(54321, 0xBABE); + noRelatedSym.set(financingDetails); + + assertEquals("USDCAD", noRelatedSym.getSymbol().getValue()); + assertEquals(Product.CURRENCY, noRelatedSym.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, noRelatedSym.getSecurityType().getValue()); + assertEquals("XYZ", noRelatedSym.getString(12345)); + assertEquals(0xBABE, noRelatedSym.getInt(54321)); + + assertEquals("CAD", noRelatedSym.getFinancingDetails().getAgreementCurrency().getValue()); + assertEquals("GHI", noRelatedSym.getString(111222)); + } + + @Test + public void shouldNotGetComponentWithCustomTags() throws FieldNotFound { + Instrument instrument = new Instrument(); + instrument.set(new Symbol("EURUSD")); + instrument.set(new Product(Product.CURRENCY)); + instrument.set(new SecurityType(SecurityType.FOREIGN_EXCHANGE_CONTRACT)); + instrument.setString(12345, "ABC"); + instrument.setInt(54321, 0xCAFE); + + FinancingDetails financingDetails = new FinancingDetails(); + financingDetails.set(new AgreementCurrency("USD")); + financingDetails.setString(111222, "DEF"); + + QuoteRequest.NoRelatedSym noRelatedSym = new QuoteRequest.NoRelatedSym(); + noRelatedSym.set(instrument); + noRelatedSym.set(financingDetails); + + instrument = noRelatedSym.getInstrument(); + + assertEquals("EURUSD", instrument.getSymbol().getValue()); + assertEquals(Product.CURRENCY, instrument.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, instrument.getSecurityType().getValue()); + assertFalse(instrument.isSetField(12345)); + assertFalse(instrument.isSetField(54321)); + + financingDetails = noRelatedSym.getFinancingDetails(); + + assertEquals("USD", financingDetails.getAgreementCurrency().getValue()); + assertFalse(instrument.isSetField(111222)); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/MessageComponentTestFixLatest.java b/quickfixj-core/src/test/java/quickfix/MessageComponentTestFixLatest.java new file mode 100644 index 0000000000..b2ce954ab1 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/MessageComponentTestFixLatest.java @@ -0,0 +1,162 @@ +package quickfix; + +import org.junit.Test; +import quickfix.field.AgreementCurrency; +import quickfix.field.Product; +import quickfix.field.SecurityType; +import quickfix.field.Symbol; +import quickfix.fixlatest.QuoteRequest; +import quickfix.fixlatest.component.FinancingDetails; +import quickfix.fixlatest.component.Instrument; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class MessageComponentTestFixLatest { + + @Test + public void shouldCopyCustomTagsToComponent() throws FieldNotFound { + Instrument instrument1 = new Instrument(); + instrument1.set(new Symbol("EURUSD")); + instrument1.set(new Product(Product.CURRENCY)); + instrument1.set(new SecurityType(SecurityType.FOREIGN_EXCHANGE_CONTRACT)); + instrument1.setString(12345, "ABC"); + instrument1.setInt(54321, 0xCAFE); + + assertEquals("EURUSD", instrument1.getSymbol().getValue()); + assertEquals(Product.CURRENCY, instrument1.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, instrument1.getSecurityType().getValue()); + assertEquals("ABC", instrument1.getString(12345)); + assertEquals(0xCAFE, instrument1.getInt(54321)); + + Instrument instrument2 = new Instrument(); + instrument1.copyTo(instrument2); + + assertEquals("EURUSD", instrument2.getSymbol().getValue()); + assertEquals(Product.CURRENCY, instrument2.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, instrument2.getSecurityType().getValue()); + + assertEquals("ABC", instrument2.getString(12345)); + assertEquals(0xCAFE, instrument2.getInt(54321)); + } + + @Test + public void shouldNotCopyCustomTagsFromComponent() throws FieldNotFound { + Instrument instrument1 = new Instrument(); + instrument1.set(new Symbol("EURUSD")); + instrument1.set(new Product(Product.CURRENCY)); + instrument1.set(new SecurityType(SecurityType.FOREIGN_EXCHANGE_CONTRACT)); + instrument1.setString(12345, "ABC"); + instrument1.setInt(54321, 0xCAFE); + + assertEquals("EURUSD", instrument1.getSymbol().getValue()); + assertEquals(Product.CURRENCY, instrument1.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, instrument1.getSecurityType().getValue()); + assertEquals("ABC", instrument1.getString(12345)); + assertEquals(0xCAFE, instrument1.getInt(54321)); + + Instrument instrument2 = new Instrument(); + instrument2.copyFrom(instrument1); + + assertEquals("EURUSD", instrument2.getSymbol().getValue()); + assertEquals(Product.CURRENCY, instrument2.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, instrument2.getSecurityType().getValue()); + + assertFalse(instrument2.isSetField(12345)); + assertFalse(instrument2.isSetField(54321)); + } + + @Test + public void shouldSetComponentWithCustomTags() throws FieldNotFound { + Instrument instrument = new Instrument(); + instrument.set(new Symbol("EURUSD")); + instrument.set(new Product(Product.CURRENCY)); + instrument.set(new SecurityType(SecurityType.FOREIGN_EXCHANGE_CONTRACT)); + instrument.setString(12345, "ABC"); + instrument.setInt(54321, 0xCAFE); + + FinancingDetails financingDetails = new FinancingDetails(); + financingDetails.set(new AgreementCurrency("USD")); + financingDetails.setString(111222, "DEF"); + + QuoteRequest.NoRelatedSym noRelatedSym = new QuoteRequest.NoRelatedSym(); + noRelatedSym.set(instrument); + noRelatedSym.set(financingDetails); + + assertEquals("EURUSD", noRelatedSym.getInstrumentComponent().getSymbol().getValue()); + assertEquals(Product.CURRENCY, noRelatedSym.getInstrumentComponent().getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, noRelatedSym.getInstrumentComponent().getSecurityType().getValue()); + assertEquals("ABC", noRelatedSym.getString(12345)); + assertEquals(0xCAFE, noRelatedSym.getInt(54321)); + + assertEquals("USD", noRelatedSym.getFinancingDetailsComponent().getAgreementCurrency().getValue()); + assertEquals("DEF", noRelatedSym.getString(111222)); + } + + @Test + public void shouldOverrideCustomComponentTags() throws FieldNotFound { + Instrument instrument = new Instrument(); + instrument.set(new Symbol("EURUSD")); + instrument.set(new Product(Product.CURRENCY)); + instrument.set(new SecurityType(SecurityType.FOREIGN_EXCHANGE_CONTRACT)); + instrument.setString(12345, "ABC"); + instrument.setInt(54321, 0xCAFE); + + FinancingDetails financingDetails = new FinancingDetails(); + financingDetails.set(new AgreementCurrency("USD")); + financingDetails.setString(111222, "DEF"); + + QuoteRequest.NoRelatedSym noRelatedSym = new QuoteRequest.NoRelatedSym(); + noRelatedSym.set(instrument); + noRelatedSym.set(financingDetails); + + instrument.set(new Symbol("USDCAD")); + instrument.setString(12345, "XYZ"); + noRelatedSym.set(instrument); + + financingDetails.set(new AgreementCurrency("CAD")); + financingDetails.setString(111222, "GHI"); + financingDetails.setInt(54321, 0xBABE); + noRelatedSym.set(financingDetails); + + assertEquals("USDCAD", noRelatedSym.getInstrumentComponent().getSymbol().getValue()); + assertEquals(Product.CURRENCY, noRelatedSym.getInstrumentComponent().getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, noRelatedSym.getInstrumentComponent().getSecurityType().getValue()); + assertEquals("XYZ", noRelatedSym.getString(12345)); + assertEquals(0xBABE, noRelatedSym.getInt(54321)); + + assertEquals("CAD", noRelatedSym.getFinancingDetailsComponent().getAgreementCurrency().getValue()); + assertEquals("GHI", noRelatedSym.getString(111222)); + } + + @Test + public void shouldNotGetComponentWithCustomTags() throws FieldNotFound { + Instrument instrument = new Instrument(); + instrument.set(new Symbol("EURUSD")); + instrument.set(new Product(Product.CURRENCY)); + instrument.set(new SecurityType(SecurityType.FOREIGN_EXCHANGE_CONTRACT)); + instrument.setString(12345, "ABC"); + instrument.setInt(54321, 0xCAFE); + + FinancingDetails financingDetails = new FinancingDetails(); + financingDetails.set(new AgreementCurrency("USD")); + financingDetails.setString(111222, "DEF"); + + QuoteRequest.NoRelatedSym noRelatedSym = new QuoteRequest.NoRelatedSym(); + noRelatedSym.set(instrument); + noRelatedSym.set(financingDetails); + + instrument = noRelatedSym.getInstrumentComponent(); + + assertEquals("EURUSD", instrument.getSymbol().getValue()); + assertEquals(Product.CURRENCY, instrument.getProduct().getValue()); + assertEquals(SecurityType.FOREIGN_EXCHANGE_CONTRACT, instrument.getSecurityType().getValue()); + assertFalse(instrument.isSetField(12345)); + assertFalse(instrument.isSetField(54321)); + + financingDetails = noRelatedSym.getFinancingDetailsComponent(); + + assertEquals("USD", financingDetails.getAgreementCurrency().getValue()); + assertFalse(instrument.isSetField(111222)); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/MessageCrackerTest.java b/quickfixj-core/src/test/java/quickfix/MessageCrackerTest.java index 2056517d7a..8b56101361 100644 --- a/quickfixj-core/src/test/java/quickfix/MessageCrackerTest.java +++ b/quickfixj-core/src/test/java/quickfix/MessageCrackerTest.java @@ -22,12 +22,9 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.stub; import java.io.InvalidObjectException; -import org.junit.Before; import org.junit.Test; import quickfix.MessageCracker.RedundantHandlerException; @@ -41,14 +38,6 @@ public class MessageCrackerTest { private int messageCracked; - private Session mockSession; - - @Before - public void setUp() throws Exception { - mockSession = mock(Session.class); - stub(mockSession.getTargetDefaultApplicationVersionID()).toReturn( - new ApplVerID(ApplVerID.FIX50SP2)); - } @Test(expected=UnsupportedMessageType.class) public void testInvokerException1() throws Exception { @@ -237,12 +226,27 @@ public void onMessage(quickfix.fix44.Email email, SessionID sessionID) { assertTrue(messageCracked > 0); } + + @Test + public void testFixtMessageCrackingWithFixLatestApplVerID() throws Exception { + quickfix.fixlatest.Email message = createFixLatestEmail(); + message.getHeader().setString(ApplVerID.FIELD, ApplVerID.FIXLATEST); + + MessageCracker cracker = new MessageCracker() { + @SuppressWarnings("unused") + public void onMessage(quickfix.fixlatest.Email email, SessionID sessionID) { + messageCracked++; + } + }; + + cracker.crack(message, new SessionID(FixVersions.BEGINSTRING_FIXT11, "SENDER", "TARGET")); + + assertTrue(messageCracked > 0); + } @Test public void testFixtMessageCrackingWithSessionDefaultApplVerID() throws Exception { quickfix.fix44.Email message = createFix44Email(); - stub(mockSession.getTargetDefaultApplicationVersionID()).toReturn( - new ApplVerID(ApplVerID.FIX44)); MessageCracker cracker = new MessageCracker() { @SuppressWarnings("unused") @@ -288,4 +292,12 @@ private quickfix.fix44.Email createFix44Email() { message.getHeader().setString(TargetCompID.FIELD, "TARGET"); return message; } + + private quickfix.fixlatest.Email createFixLatestEmail() { + quickfix.fixlatest.Email message = new quickfix.fixlatest.Email(); + message.getHeader().setString(BeginString.FIELD, FixVersions.BEGINSTRING_FIXT11); + message.getHeader().setString(SenderCompID.FIELD, "SENDER"); + message.getHeader().setString(TargetCompID.FIELD, "TARGET"); + return message; + } } diff --git a/quickfixj-core/src/test/java/quickfix/MessageTest.java b/quickfixj-core/src/test/java/quickfix/MessageTest.java index 81671750d2..194f4d39bb 100644 --- a/quickfixj-core/src/test/java/quickfix/MessageTest.java +++ b/quickfixj-core/src/test/java/quickfix/MessageTest.java @@ -19,15 +19,28 @@ package quickfix; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +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.time.LocalDateTime; +import java.time.ZoneOffset; + +import javax.xml.parsers.DocumentBuilderFactory; + import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; 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; import quickfix.field.BidType; @@ -38,7 +51,6 @@ import quickfix.field.CrossID; import quickfix.field.CrossPrioritization; import quickfix.field.CrossType; -import quickfix.field.CstmApplVerID; import quickfix.field.CumQty; import quickfix.field.EncodedText; import quickfix.field.EncodedTextLen; @@ -63,7 +75,9 @@ import quickfix.field.MsgDirection; import quickfix.field.MsgSeqNum; import quickfix.field.MsgType; +import quickfix.field.NoHops; import quickfix.field.NoOrders; +import quickfix.field.NoSides; import quickfix.field.OrdStatus; import quickfix.field.OrdType; import quickfix.field.OrderID; @@ -78,7 +92,6 @@ import quickfix.field.RawData; import quickfix.field.RawDataLength; import quickfix.field.RefMsgType; -import quickfix.field.SecureData; import quickfix.field.SecurityID; import quickfix.field.SecurityIDSource; import quickfix.field.SecurityReqID; @@ -94,7 +107,6 @@ import quickfix.field.StrikePrice; import quickfix.field.Symbol; import quickfix.field.TargetCompID; -import quickfix.field.TargetSubID; import quickfix.field.Text; import quickfix.field.TotNoOrders; import quickfix.field.TradeDate; @@ -117,164 +129,27 @@ 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.assertArrayEquals; -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; - public class MessageTest { @Rule public ExpectedException expectedException = ExpectedException.none(); - @Test - public void testRepeatingField() throws Exception { - final Message m = new Message( - "8=FIX.4.0\0019=100\00135=D\00134=2\00149=TW\00156=ISLD\00111=ID\00121=1\001" - + "40=1\00154=1\00140=2\00138=200\00155=INTC\00110=160\001"); - assertFalse("message should be invalid", m.hasValidStructure()); - assertEquals("wrong invalid tag", 40, m.getInvalidTag()); - } - @Test public void testTrailerFieldOrdering() throws Exception { final NewOrderSingle order = createNewOrderSingle(); - + order.getTrailer().setField(new Signature("FOO")); order.getTrailer().setField(new SignatureLength(3)); - + assertTrue(order.toString().contains("93=3\00189=FOO\001")); } - + private NewOrderSingle createNewOrderSingle() { return new NewOrderSingle(new ClOrdID("CLIENT"), new HandlInst( - HandlInst.AUTOMATED_EXECUTION_ORDER_PUBLIC_BROKER_INTERVENTION_OK), new Symbol("ORCL"), - new Side(Side.BUY), new TransactTime(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC)), new OrdType(OrdType.LIMIT)); - } - - @Test - public void testHeaderCustomFieldOrdering() throws Exception { - - class MyMessage extends Message { - - final int[] headerFieldOrder = { - BeginString.FIELD, - BodyLength.FIELD, - MsgType.FIELD, - TargetSubID.FIELD, - SendingTime.FIELD, - MsgSeqNum.FIELD, - SenderCompID.FIELD, - TargetCompID.FIELD - }; - - public MyMessage() { - super(); - header = new Header(headerFieldOrder); - } - } - - final MyMessage myMessage = new MyMessage(); - - myMessage.getHeader().setField(new SenderCompID("foo")); - myMessage.getHeader().setField(new MsgSeqNum(22)); - myMessage.getHeader().setString(SendingTime.FIELD, "20120922-11:00:00"); - myMessage.getHeader().setField(new TargetCompID("bar")); - - assertTrue(myMessage.toString().contains("52=20120922-11:00:00\00134=22\00149=foo\00156=bar")); + HandlInst.AUTOMATED_EXECUTION_INTERVENTION_OK), new Symbol("ORCL"), + new Side(Side.BUY), new TransactTime(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC)), new OrdType(OrdType.LIMIT)); } - - @Test - public void testHeaderFieldWithCustomTransportDictionaryConstructorReadsHeaderField() throws Exception { - - final DataDictionary customSessionDictionary = new DataDictionary("FIXT11_Custom_Test.xml"); - customSessionDictionary.setAllowUnknownMessageFields(false); - - final DataDictionary standardSessionDictionary = new DataDictionary("FIXT11.xml"); - standardSessionDictionary.setAllowUnknownMessageFields(false); - - final DataDictionary applicationDictionary = new DataDictionary("FIX50.xml"); - applicationDictionary.setAllowUnknownMessageFields(false); - - final String sep = "\001"; - final StringBuilder sb = new StringBuilder(); - sb.append("8=FIXT1.1"); - sb.append(sep); - sb.append("9=112"); - sb.append(sep); - sb.append("35=6"); - sb.append(sep); - sb.append("49=SENDER_COMP_ID"); - sb.append(sep); - sb.append("56=TARGET_COMP_ID"); - sb.append(sep); - sb.append("34=20"); - sb.append(sep); - sb.append("52=20120922-11:00:00"); - sb.append(sep); - sb.append("12312=foo"); - sb.append(sep); - sb.append("23=123456"); - sb.append(sep); - sb.append("28=N"); - sb.append(sep); - sb.append("55=[N/A]"); - sb.append(sep); - sb.append("54=1"); - sb.append(sep); - sb.append("27=U"); - sb.append(sep); - sb.append("10=52"); - sb.append(sep); - final String messageData = sb.toString(); - - final Message standardMessage = new Message(messageData, standardSessionDictionary, applicationDictionary, true); - - // Test that field is in body not the header - assertTrue(standardMessage.toString().contains("12312=foo")); - assertFalse(standardMessage.getHeader().isSetField(12312)); - assertTrue(standardMessage.isSetField(12312)); - assertEquals("foo", standardMessage.getString(12312)); - - // Test that field is correctly classified in header with customSessionDictionary - final Message customMessage = new Message(messageData, customSessionDictionary, applicationDictionary, true); - assertTrue(customMessage.toString().contains("12312=foo")); - assertTrue(customMessage.getHeader().isSetField(12312)); - assertEquals("foo", customMessage.getHeader().getString(12312)); - assertFalse(customMessage.isSetField(12312)); - } - - @Test - public void testTrailerCustomFieldOrdering() throws Exception { - - class MyMessage extends Message { - - final int[] trailerFieldOrder = { Signature.FIELD, SignatureLength.FIELD, CheckSum.FIELD }; - - public MyMessage() { - super(); - trailer = new Trailer(trailerFieldOrder); - } - } - - final MyMessage myMessage = new MyMessage(); - - myMessage.getTrailer().setField(new Signature("FOO")); - myMessage.getTrailer().setField(new SignatureLength(3)); - assertTrue(myMessage.toString().contains("89=FOO\00193=3\001")); - } - @Test public void testHeaderGroupParsing() throws Exception { final Message message = new Message("8=FIX.4.2\0019=40\00135=A\001" @@ -293,7 +168,7 @@ public void testHeaderGroupParsing() throws Exception { public void testEmbeddedMessage() throws Exception { final ExecutionReport report = new ExecutionReport(new OrderID("ORDER"), - new ExecID("EXEC"), new ExecType(ExecType.FILL), new OrdStatus(OrdStatus.FILLED), + new ExecID("EXEC"), new ExecType(ExecType.TRADE), new OrdStatus(OrdStatus.FILLED), new Side(Side.BUY), new LeavesQty(100), new CumQty(100), new AvgPx(50)); final NewOrderSingle order = createNewOrderSingle(); @@ -314,7 +189,7 @@ private void doTestMessageWithEncodedField(String charset, String text) throws E final Message msg = new Message(order.toString(), DataDictionaryTest.getDictionary()); assertEquals(charset + " encoded field", text, msg.getString(EncodedText.FIELD)); } finally { - CharsetSupport.setCharset(CharsetSupport.getDefaultCharset()); + CharsetSupport.setDefaultCharset(); } } @@ -383,37 +258,14 @@ public void testParsing2() throws Exception { assertEquals("wrong value", "CAD", valueMessageType.getString(UnderlyingCurrency.FIELD)); } - @Test - public void testParseEmptyString() throws Exception { - final String data = ""; - - // with validation - try { - new Message(data, DataDictionaryTest.getDictionary()); - } catch (final InvalidMessage im) { - } catch (final Throwable e) { - e.printStackTrace(); - fail("InvalidMessage expected, got " + e.getClass().getName()); - } - - // without validation - try { - new Message(data, DataDictionaryTest.getDictionary(), false); - } catch (final InvalidMessage im) { - } catch (final Throwable e) { - e.printStackTrace(); - fail("InvalidMessage expected, got " + e.getClass().getName()); - } - } - @Test public void testValidation() throws Exception { final String data = "8=FIX.4.4\0019=309\00135=8\00149=ASX\00156=CL1_FIX44\00134=4\001" + - "52=20060324-01:05:58\00117=X-B-WOW-1494E9A0:58BD3F9D-1109\001150=D\00139=0\001" + - "11=184271\00138=200\001198=1494E9A0:58BD3F9D\001526=4324\00137=B-WOW-1494E9A0:58BD3F9D\001" + - "55=WOW\00154=1\001151=200\00114=0\00140=2\00144=15\00159=1\0016=0\001453=3\001448=AAA35791\001" + - "447=D\001452=3\001448=8\001447=D\001452=4\001448=FIX11\001" + - "447=D\001452=36\00160=20060320-03:34:29\00110=169\001"; + "52=20060324-01:05:58\00117=X-B-WOW-1494E9A0:58BD3F9D-1109\001150=D\00139=0\001" + + "11=184271\00138=200\001198=1494E9A0:58BD3F9D\001526=4324\00137=B-WOW-1494E9A0:58BD3F9D\001" + + "55=WOW\00154=1\001151=200\00114=0\00140=2\00144=15\00159=1\0016=0\001453=3\001448=AAA35791\001" + + "447=D\001452=3\001448=8\001447=D\001452=4\001448=FIX11\001" + + "447=D\001452=36\00160=20060320-03:34:29\00110=169\001"; final ExecutionReport executionReport = new ExecutionReport(); final DataDictionary dictionary = DataDictionaryTest.getDictionary(); assertNotNull(dictionary); @@ -425,17 +277,17 @@ public void testValidation() throws Exception { // QFJ-675: Message.clear() should reset position field to zero to enable Message to be reused public void testParseTwice() throws Exception { final String data1 = "8=FIX.4.4\0019=309\00135=8\00149=ASX\00156=CL1_FIX44\00134=4\001" + - "52=20060324-01:05:58\00117=X-B-WOW-1494E9A0:58BD3F9D-1109\001150=D\00139=0\001" + - "11=184271\00138=200\001198=1494E9A0:58BD3F9D\001526=4324\00137=B-WOW-1494E9A0:58BD3F9D\001" + - "55=WOW\00154=1\001151=200\00114=0\00140=2\00144=15\00159=1\0016=0\001453=3\001448=AAA35791\001" + - "447=D\001452=3\001448=8\001447=D\001452=4\001448=FIX11\001" + - "447=D\001452=36\00160=20060320-03:34:29\00110=169\001"; + "52=20060324-01:05:58\00117=X-B-WOW-1494E9A0:58BD3F9D-1109\001150=D\00139=0\001" + + "11=184271\00138=200\001198=1494E9A0:58BD3F9D\001526=4324\00137=B-WOW-1494E9A0:58BD3F9D\001" + + "55=WOW\00154=1\001151=200\00114=0\00140=2\00144=15\00159=1\0016=0\001453=3\001448=AAA35791\001" + + "447=D\001452=3\001448=8\001447=D\001452=4\001448=FIX11\001" + + "447=D\001452=36\00160=20060320-03:34:29\00110=169\001"; final String data2 = "8=FIX.4.4\0019=309\00135=8\00149=ASX\00156=CL1_FIX44\00134=4\001" + - "52=20060324-01:05:58\00117=X-B-WOW-1494E9A0:58BD3F9D-1109\001150=D\00139=0\00111=123456\001" + - "38=200\001198=1494E9A0:58BD3F9D\001526=4324\00137=B-WOW-1494E9A0:58BD3F9D\00155=WOW\00154=1\001" + - "151=200\00114=0\00140=2\00144=15\00159=1\0016=0\001453=3\001448=AAA35791\001447=D\001452=3\001" + - "448=8\001447=D\001452=4\001448=FIX11\001447=D\001452=36\00160=20060320-03:34:29\00110=167\001"; + "52=20060324-01:05:58\00117=X-B-WOW-1494E9A0:58BD3F9D-1109\001150=D\00139=0\00111=123456\001" + + "38=200\001198=1494E9A0:58BD3F9D\001526=4324\00137=B-WOW-1494E9A0:58BD3F9D\00155=WOW\00154=1\001" + + "151=200\00114=0\00140=2\00144=15\00159=1\0016=0\001453=3\001448=AAA35791\001447=D\001452=3\001" + + "448=8\001447=D\001452=4\001448=FIX11\001447=D\001452=36\00160=20060320-03:34:29\00110=167\001"; final DataDictionary dictionary = DataDictionaryTest.getDictionary(); final ExecutionReport executionReport = new ExecutionReport(); @@ -453,10 +305,10 @@ public void testParseTwice() throws Exception { // QFJ-426 Message header will not validate when containing 'Hop' group public void testValidationWithHops() throws Exception { final String data = "8=FIX.4.4\0019=309\00135=8\00149=ASX\00156=CL1_FIX44\00134=4\001" + - "52=20060324-01:05:58\00117=X-B-WOW-1494E9A0:58BD3F9D-1109\001150=D\00139=0\00111=184271\001" + - "38=200\001198=1494E9A0:58BD3F9D\001526=4324\00137=B-WOW-1494E9A0:58BD3F9D\00155=WOW\00154=1\001" + - "151=200\00114=0\00140=2\00144=15\00159=1\0016=0\001453=3\001448=AAA35791\001447=D\001452=3\001" + - "448=8\001447=D\001452=4\001448=FIX11\001447=D\001452=36\00160=20060320-03:34:29\00110=169\001"; + "52=20060324-01:05:58\00117=X-B-WOW-1494E9A0:58BD3F9D-1109\001150=D\00139=0\00111=184271\001" + + "38=200\001198=1494E9A0:58BD3F9D\001526=4324\00137=B-WOW-1494E9A0:58BD3F9D\00155=WOW\00154=1\001" + + "151=200\00114=0\00140=2\00144=15\00159=1\0016=0\001453=3\001448=AAA35791\001447=D\001452=3\001" + + "448=8\001447=D\001452=4\001448=FIX11\001447=D\001452=36\00160=20060320-03:34:29\00110=169\001"; final ExecutionReport executionReport = new ExecutionReport(); final DataDictionary dictionary = DataDictionaryTest.getDictionary(); assertNotNull(dictionary); @@ -472,8 +324,8 @@ public void testValidationWithHops() throws Exception { @Test public void testAppMessageValidation() throws Exception { final String data = "8=FIXT.1.1\0019=234\00135=W\00134=2\00149=ABFX\00152=20080722-16:37:11.234\001" + - "56=X2RV1\00155=EUR/USD\001262=CAP0000011\001268=2\001269=0\001270=1.57844\00115=EUR\001" + - "271=500000\001272=20080724\001269=1\001270=1.57869\00115=EUR\001271=500000\001272=20080724\00110=097\001"; + "56=X2RV1\00155=EUR/USD\001262=CAP0000011\001268=2\001269=0\001270=1.57844\00115=EUR\001" + + "271=500000\001272=20080724\001269=1\001270=1.57869\00115=EUR\001271=500000\001272=20080724\00110=097\001"; final MarketDataSnapshotFullRefresh mdsfr = new MarketDataSnapshotFullRefresh(); final DataDictionary sessDictionary = DataDictionaryTest.getDictionary("FIXT11.xml"); final DataDictionary appDictionary = DataDictionaryTest.getDictionary("FIX50.xml"); @@ -482,11 +334,25 @@ public void testAppMessageValidation() throws Exception { mdsfr.fromString(data, sessDictionary, appDictionary, true); DataDictionary.validate(mdsfr, sessDictionary, appDictionary); } + + @Test + public void testAppMessageValidationFixLatest() throws Exception { + final String data = "8=FIXT.1.1\0019=234\00135=W\00134=2\00149=ABFX\00152=20080722-16:37:11.234\001" + + "56=X2RV1\00155=EUR/USD\001262=CAP0000011\001779=20080722-16:37:11.234\001268=2\001269=0\001270=1.57844\00115=EUR\001" + + "271=500000\001272=20080724\001269=1\001270=1.57869\00115=EUR\001271=500000\001272=20080724\00110=118\001"; + final quickfix.fixlatest.MarketDataSnapshotFullRefresh mdsfr = new quickfix.fixlatest.MarketDataSnapshotFullRefresh(); + final DataDictionary sessDictionary = DataDictionaryTest.getDictionary("FIXT11.xml"); + final DataDictionary appDictionary = DataDictionaryTest.getDictionary("FIXLatest.xml"); + assertNotNull(sessDictionary); + assertNotNull(appDictionary); + mdsfr.fromString(data, sessDictionary, appDictionary, true); + DataDictionary.validate(mdsfr, sessDictionary, appDictionary); + } @Test public void testAdminMessageValidation() throws Exception { final String data = "8=FIXT.1.1\0019=84\00135=A\00149=EXEC\00156=BANZAI\00134=1\001" + - "52=20080811-13:26:12.409\001108=1\001141=Y\00198=0\0011137=7\00110=102\001"; + "52=20080811-13:26:12.409\001108=1\001141=Y\00198=0\0011137=7\00110=102\001"; final Logon logon = new Logon(); final DataDictionary sessionDictionary = DataDictionaryTest.getDictionary("FIXT11.xml"); final DataDictionary appDictionary = DataDictionaryTest.getDictionary("FIX50.xml"); @@ -503,7 +369,7 @@ public void testGroupDelimOrdering() throws Exception { final Group partyGroup = new Group(quickfix.field.NoPartyIDs.FIELD, PartyID.FIELD); partyGroup.setField(new PartyID("TraderName")); partyGroup.setField(new PartyIDSource( - PartyIDSource.GENERALLY_ACCEPTED_MARKET_PARTICIPANT_IDENTIFIER)); + PartyIDSource.GENERAL_IDENTIFIER)); partyGroup.setField(new PartyRole(11)); order.addGroup(partyGroup); final String data = order.toString(); @@ -538,15 +404,6 @@ public void testComponentGroupInsertion() throws Exception { assertEquals("wrong # of party IDs", 2, order.getNoPartyIDs().getValue()); } - // QFJ-66 Should not throw exception when parsing data field in header - @Test - public void testHeaderDataField() throws Exception { - final Message m = new Message("8=FIX.4.2\0019=53\00135=A\00190=4\00191=ABCD\001" - + "98=0\001384=2\001372=D\001385=R\001372=8\001385=S\00110=241\001", - DataDictionaryTest.getDictionary()); - assertEquals("ABCD", m.getHeader().getString(SecureData.FIELD)); - } - // QFJ-52 @Test public void testInvalidFirstFieldInGroup() throws Exception { @@ -579,40 +436,6 @@ public void testRequiredGroupValidation() throws Exception { } } - /** - * Test for data fields with SOH. This test is based on report from a user on - * the QuickFIX mailing list. The problem was the user's configuration but this - * seems like a good unit test to keep in the suite. - */ - @Test - public void testDataFieldParsing() throws Exception { - final String data = "10001=Canonical.1.00\00110002=001058\00125001=01\00110003=SAPI_ADMRESP\00110004=SUBSCRIBE_RESP\001" - + "10009=705\00110012=01\00110005=SPGW\00110006=SAPI\00110007=0\00110010=16:25:11.537\001" - + "10045=SDQADL:01:/SDB/ENT/@/@/STKSDLL:7\00110955=Y\00110963=043\00110961=03\00111285=N\001" - + "11339=823,980\00110919=N\00111111=86795696\00110898=043\00110920=~\00110938=N\00111340=5- 9.99\001" - + "11343=0.20\00111344=~\00111341=~\00111342=0.15\00111345=10- 14.99\00111348=0.25\00111349=~\00111346=~\001" - + "11347=0.15\00111350=15- 19.99\00111353=0.30\00111354=~\00111351=~\00111352=0.20\00111338=23SEP05\001" - + "10981=0\00110485=N\00110761=0\00111220=~\00111224=N\00110808=N\00110921=~\00110960=N\00110957=N\00111329=N\001" - + "11286=0\00111214=USA\00110917=Y\00111288=0\00110906=N\00110737=0.01\00110956=~\00110967=~\00110965=~\00110809=0\001" - + "10762=N\00110763=N\00110712=1\00110905=09:30:00\00110918=YA0101\00110951=Y\00110469=1\00110949=1\00110487=Q\00110950=Y\001" - + "10899=N\00110380=N\00110696=03\00111082=18.41\00110217=12\00110954=N\00110708=E\00110958=N\00111213=US \00111334=N\001" - + "11332=N\00111331=N\00111330=N\00111335=N\00111333=N\00110767=3\00110974=~\00110980=AIRTRAN HOLDINGS \00111289=N\001" - + "10912=4\00110915=0501\00110914=0501\00110975=N\00110913=SLK\00110698=055\00110666=AAI\00110903=S\00111328=N\001" - + "10624=L\00111287=0\00110699=0\00110962=L\00111227=SUB1\00111229=5\00111228=1\00111236=16:24:41.521\00111277=16:25:11.630\001"; - - try { - final DataDictionary dictionary = DataDictionaryTest.getDictionary(); - final Message m = new Message(("8=FIX.4.4\0019=1144\00135=A\001" - + "98=0\001384=2\001372=D\001385=R\001372=8\001385=S\00195=1092\001" + "96=" - + data + "\00110=5\001"), dictionary); - assertEquals(1144, m.bodyLength()); - final Message m2 = new Message(m.toString(), dictionary); - assertEquals(1144, m2.bodyLength()); - } catch (final InvalidMessage e) { - fail(e.getMessage()); - } - } - /** * Test for data fields with SOH. This test is based on report from a user on * the QuickFIX mailing list. The problem was the user's configuration but this @@ -621,18 +444,18 @@ public void testDataFieldParsing() throws Exception { @Test public void testDataFieldWithManualFieldInsertion() throws Exception { final String data = "10001=Canonical.1.00\00110002=001058\00125001=01\00110003=SAPI_ADMRESP\00110004=SUBSCRIBE_RESP\001" - + "10009=705\00110012=01\00110005=SPGW\00110006=SAPI\00110007=0\00110010=16:25:11.537\001" - + "10045=SDQADL:01:/SDB/ENT/@/@/STKSDLL:7\00110955=Y\00110963=043\00110961=03\00111285=N\001" - + "11339=823,980\00110919=N\00111111=86795696\00110898=043\00110920=~\00110938=N\00111340=5- 9.99\001" - + "11343=0.20\00111344=~\00111341=~\00111342=0.15\00111345=10- 14.99\00111348=0.25\00111349=~\00111346=~\001" - + "11347=0.15\00111350=15- 19.99\00111353=0.30\00111354=~\00111351=~\00111352=0.20\00111338=23SEP05\001" - + "10981=0\00110485=N\00110761=0\00111220=~\00111224=N\00110808=N\00110921=~\00110960=N\00110957=N\00111329=N\001" - + "11286=0\00111214=USA\00110917=Y\00111288=0\00110906=N\00110737=0.01\00110956=~\00110967=~\00110965=~\00110809=0\001" - + "10762=N\00110763=N\00110712=1\00110905=09:30:00\00110918=YA0101\00110951=Y\00110469=1\00110949=1\00110487=Q\00110950=Y\001" - + "10899=N\00110380=N\00110696=03\00111082=18.41\00110217=12\00110954=N\00110708=E\00110958=N\00111213=US \00111334=N\001" - + "11332=N\00111331=N\00111330=N\00111335=N\00111333=N\00110767=3\00110974=~\00110980=AIRTRAN HOLDINGS \00111289=N\001" - + "10912=4\00110915=0501\00110914=0501\00110975=N\00110913=SLK\00110698=055\00110666=AAI\00110903=S\00111328=N\001" - + "10624=L\00111287=0\00110699=0\00110962=L\00111227=SUB1\00111229=5\00111228=1\00111236=16:24:41.521\00111277=16:25:11.630\001"; + + "10009=705\00110012=01\00110005=SPGW\00110006=SAPI\00110007=0\00110010=16:25:11.537\001" + + "10045=SDQADL:01:/SDB/ENT/@/@/STKSDLL:7\00110955=Y\00110963=043\00110961=03\00111285=N\001" + + "11339=823,980\00110919=N\00111111=86795696\00110898=043\00110920=~\00110938=N\00111340=5- 9.99\001" + + "11343=0.20\00111344=~\00111341=~\00111342=0.15\00111345=10- 14.99\00111348=0.25\00111349=~\00111346=~\001" + + "11347=0.15\00111350=15- 19.99\00111353=0.30\00111354=~\00111351=~\00111352=0.20\00111338=23SEP05\001" + + "10981=0\00110485=N\00110761=0\00111220=~\00111224=N\00110808=N\00110921=~\00110960=N\00110957=N\00111329=N\001" + + "11286=0\00111214=USA\00110917=Y\00111288=0\00110906=N\00110737=0.01\00110956=~\00110967=~\00110965=~\00110809=0\001" + + "10762=N\00110763=N\00110712=1\00110905=09:30:00\00110918=YA0101\00110951=Y\00110469=1\00110949=1\00110487=Q\00110950=Y\001" + + "10899=N\00110380=N\00110696=03\00111082=18.41\00110217=12\00110954=N\00110708=E\00110958=N\00111213=US \00111334=N\001" + + "11332=N\00111331=N\00111330=N\00111335=N\00111333=N\00110767=3\00110974=~\00110980=AIRTRAN HOLDINGS \00111289=N\001" + + "10912=4\00110915=0501\00110914=0501\00110975=N\00110913=SLK\00110698=055\00110666=AAI\00110903=S\00111328=N\001" + + "10624=L\00111287=0\00110699=0\00110962=L\00111227=SUB1\00111229=5\00111228=1\00111236=16:24:41.521\00111277=16:25:11.630\001"; try { final DataDictionary dictionary = DataDictionaryTest.getDictionary(); @@ -650,17 +473,6 @@ public void testDataFieldWithManualFieldInsertion() throws Exception { } } - @Test - public void testFix5HeaderFields() { - assertTrue(Message.isHeaderField(ApplVerID.FIELD)); - 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(); @@ -678,7 +490,7 @@ public void testCalculateStringWithNestedGroups() throws Exception { noc.setString(TransactTime.FIELD, "20060319-09:08:19"); noc.setString(CrossID.FIELD, "184214"); noc.setInt(CrossType.FIELD, - CrossType.CROSS_IOC_CROSS_TRADE_WHICH_IS_EXECUTED_PARTIALLY_AND_THE_REST_IS_CANCELLED_ONE_SIDE_IS_FULLY_EXECUTED_THE_OTHER_SIDE_IS_PARTIALLY_EXECUTED_WITH_THE_REMAINDER_BEING_CANCELLED_THIS_IS_EQUIVALENT_TO_AN_IOC_ON_THE_OTHER_SIDE_NOTE_CROSSPRIORITIZATION_FIELD_MAY_BE_USED_TO_INDICATE_WHICH_SIDE_SHOULD_FULLY_EXECUTE_IN_THIS_SCENARIO_); + CrossType.CROSS_IOC); noc.setInt(CrossPrioritization.FIELD, CrossPrioritization.NONE); final NewOrderCross.NoSides side = new NewOrderCross.NoSides(); @@ -687,13 +499,13 @@ public void testCalculateStringWithNestedGroups() throws Exception { final NewOrderCross.NoSides.NoPartyIDs party = new NewOrderCross.NoSides.NoPartyIDs(); party.setString(PartyID.FIELD, "8"); - party.setChar(PartyIDSource.FIELD, PartyIDSource.PROPRIETARY_CUSTOM_CODE); + party.setChar(PartyIDSource.FIELD, PartyIDSource.PROPRIETARY); party.setInt(PartyRole.FIELD, PartyRole.CLEARING_FIRM); side.addGroup(party); party.setString(PartyID.FIELD, "AAA35777"); - party.setChar(PartyIDSource.FIELD, PartyIDSource.PROPRIETARY_CUSTOM_CODE); + party.setChar(PartyIDSource.FIELD, PartyIDSource.PROPRIETARY); party.setInt(PartyRole.FIELD, PartyRole.CLIENT_ID); side.addGroup(party); @@ -706,30 +518,30 @@ public void testCalculateStringWithNestedGroups() throws Exception { party.clear(); party.setString(PartyID.FIELD, "8"); - party.setChar(PartyIDSource.FIELD, PartyIDSource.PROPRIETARY_CUSTOM_CODE); + party.setChar(PartyIDSource.FIELD, PartyIDSource.PROPRIETARY); party.setInt(PartyRole.FIELD, PartyRole.CLEARING_FIRM); side.addGroup(party); party.clear(); party.setString(PartyID.FIELD, "aaa"); - party.setChar(PartyIDSource.FIELD, PartyIDSource.PROPRIETARY_CUSTOM_CODE); + party.setChar(PartyIDSource.FIELD, PartyIDSource.PROPRIETARY); party.setInt(PartyRole.FIELD, PartyRole.CLIENT_ID); side.addGroup(party); noc.addGroup(side); final String expectedMessage = "8=FIX.4.4\0019=247\00135=s\00134=5\00149=sender\00152=20060319-09:08:20.881\001" - + "56=target\00122=8\00140=2\00144=9\00148=ABC\00155=ABC\00160=20060319-09:08:19\001548=184214\001549=2\001" - + "550=0\001552=2\00154=1\001453=2\001448=8\001447=D\001452=4\001448=AAA35777\001447=D\001452=3\00138=9\00154=2\001" - + "453=2\001448=8\001447=D\001452=4\001448=aaa\001447=D\001452=3\00138=9\00110=056\001"; + + "56=target\00122=8\00140=2\00144=9\00148=ABC\00155=ABC\00160=20060319-09:08:19\001548=184214\001549=2\001" + + "550=0\001552=2\00154=1\001453=2\001448=8\001447=D\001452=4\001448=AAA35777\001447=D\001452=3\00138=9\00154=2\001" + + "453=2\001448=8\001447=D\001452=4\001448=aaa\001447=D\001452=3\00138=9\00110=056\001"; assertEquals("wrong message", expectedMessage, noc.toString()); } @Test public void testFieldOrdering() throws Exception { final String expectedMessageString = "8=FIX.4.4\0019=171\00135=D\00149=SenderCompId\00156=TargetCompId\001" + - "11=183339\00122=8\00138=1\00140=2\00144=12\00148=BHP\00154=2\00155=BHP\00159=1\00160=20060223-22:38:33\001" + - "526=3620\001453=2\001448=8\001447=D\001452=4\001448=AAA35354\001447=D\001452=3\00110=168\001"; + "11=183339\00122=8\00138=1\00140=2\00144=12\00148=BHP\00154=2\00155=BHP\00159=1\00160=20060223-22:38:33\001" + + "526=3620\001453=2\001448=8\001447=D\001452=4\001448=AAA35354\001447=D\001452=3\00110=168\001"; final DataDictionary dataDictionary = new DataDictionary("FIX44.xml"); final Message message = new DefaultMessageFactory() .create(dataDictionary.getVersion(), "D"); @@ -740,63 +552,6 @@ public void testFieldOrdering() throws Exception { actualMessageString.contains("453=2\001448=8\001447=D\001452=4\001448=AAA35354\001447=D\001452=3")); } - @Test - public void testHeaderFieldsMissing() throws Exception { - try { - new Message("1=FIX.4.2"); - } catch (final InvalidMessage e) { - // expected - } - } - - @Test - public void testHeaderFieldInBody() throws Exception { - final Message message = new Message("8=FIX.4.2\0019=40\00135=A\001" - + "98=0\001212=4\001384=2\001372=D\001385=R\001372=8\001385=S\00110=103\001", - DataDictionaryTest.getDictionary()); - - assertFalse(message.hasValidStructure()); - - assertTrue(message.getHeader().isSetField(212)); - - assertEquals(SessionRejectReason.TAG_SPECIFIED_OUT_OF_REQUIRED_ORDER, message - .getException().getSessionRejectReason()); - assertEquals(212, message.getException().getField()); - } - - @Test - public void testTrailerFieldInBody() throws Exception { - final Message message = new Message("8=FIX.4.2\0019=40\00135=A\001" - + "98=0\00193=5\001384=2\001372=D\001385=R\001372=8\001385=S\00110=63\001", - DataDictionaryTest.getDictionary()); - - assertFalse(message.hasValidStructure()); - - final SignatureLength signatureLength = new SignatureLength(); - message.getTrailer().getField(signatureLength); - assertEquals(5, signatureLength.getValue()); - } - - @Test - public void testMessageFromString() { - Message message = null; - - boolean badMessage = false; - try { - message = new Message("8=FIX.4.2\0019=12\00135=A\001108=30\00110=036\001"); - } catch (final InvalidMessage e) { - badMessage = true; - } - assertTrue("Message should be invalid", badMessage); - - try { - message = new Message("8=FIX.4.2\0019=12\00135=A\001108=30\00110=026\001"); - } catch (final InvalidMessage e) { - fail("Message should be valid (" + e.getMessage() + ")"); - } - assertEquals("8=FIX.4.2\0019=12\00135=A\001108=30\00110=026\001", message.toString()); - } - @Test public void testMessageGroups() { final Message message = new Message(); @@ -805,48 +560,6 @@ public void testMessageGroups() { assertGroupContent(message, numAllocs); } - // Includes test for QFJ-413. Repeating group check for size = 0 - @Test - public void testMessageGroupCountValidation() throws Exception { - final String data = "8=FIX.4.4\0019=222\00135=D\00149=SenderCompId\00156=TargetCompId\00134=37\001" + - "52=20070223-22:28:33\00111=183339\00122=8\00138=1\00140=2\00144=12\00148=BHP\00154=2\001" + - "55=BHP\00159=1\00160=20060223-22:38:33\001526=3620\00178=0\00179=AllocACC1\00180=1010.1\001" + - "79=AllocACC2\00180=2020.2\001453=2\001448=8\001447=D\001452=4\001448=AAA35354\001447=D\001452=3\00110=079\001"; - final Message message = new Message(); - final DataDictionary dd = DataDictionaryTest.getDictionary(); - message.fromString(data, dd, true); - try { - dd.validate(message); - fail("No exception thrown"); - } catch (final FieldException e) { - final String emsg = e.getMessage(); - assertNotNull("No exception message", emsg); - assertTrue(emsg.startsWith("Incorrect NumInGroup")); - } - } - - /** - * QFJ-760 - */ - @Test - public void testMessageWithMissingChecksumField() throws Exception { - // checksum is "merged" into field 452, i.e. SOH is missing between field 452 and 10 - String badMessage = "8=FIX.4.4\0019=275\00135=D\00134=3\00149=441000-XXXXX-X-XXXX-001\001" + - "52=20131113-10:22:31.567\00156=XXXXX\0011=A1\00111=9fef3663330e209e1bce\00118=H\001" + - "22=4\00138=200\00140=M\00148=XX0005519XXXX\00154=1\00155=[N/A]\00158=MassTest\00159=0\001" + - "60=20131113-10:22:31.567\001100=XXXX\001526=9fef3663330e209e1bce\001453=1\001" + - "448=XXXXXXXX030\001447=D\001452=3610=016\001"; - - Message msg = new Message(); - try { - msg.fromString(badMessage, DataDictionaryTest.getDictionary(), true); - fail(); - } catch (final InvalidMessage e) { - final String emsg = e.getMessage(); - assertNotNull("No exception message", emsg); - assertTrue(emsg.startsWith("Field not found")); - } - } @Test public void testMessageCloneWithGroups() { @@ -862,7 +575,7 @@ public void testFieldOrderAfterClone() { final Message message = new quickfix.fix44.NewOrderSingle(); final quickfix.fix44.NewOrderSingle.NoPartyIDs partyIdGroup = new quickfix.fix44.NewOrderSingle.NoPartyIDs(); partyIdGroup.set(new PartyID("PARTY_1")); - partyIdGroup.set(new PartyIDSource(PartyIDSource.DIRECTED_BROKER_THREE_CHARACTER_ACRONYM_AS_DEFINED_IN_ISITC_ETC_BEST_PRACTICE_GUIDELINES_DOCUMENT)); + partyIdGroup.set(new PartyIDSource(PartyIDSource.ISITCACRONYM)); partyIdGroup.set(new PartyRole(PartyRole.INTRODUCING_FIRM)); message.addGroup(partyIdGroup); final Message clonedMessage = (Message) message.clone(); @@ -980,264 +693,6 @@ public void testHasGroup() { assertFalse("wrong value", message.hasGroup(3, numAllocs.getFieldTag())); } - @Test - public void testIsEmpty() { - final Message message = new Message(); - assertTrue("Message should be empty on construction", message.isEmpty()); - message.getHeader().setField(new BeginString("FIX.4.2")); - assertFalse("Header should contain a field", message.isEmpty()); - message.clear(); - assertTrue("Message should be empty after clear", message.isEmpty()); - message.setField(new Symbol("MSFT")); - assertFalse("Body should contain a field", message.isEmpty()); - message.clear(); - assertTrue("Message should be empty after clear", message.isEmpty()); - message.getTrailer().setField(new CheckSum("10")); - assertFalse("Trailer should contain a field", message.isEmpty()); - message.clear(); - assertTrue("Message should be empty after clear", message.isEmpty()); - } - - @Test - public void testMessageSetGetString() { - final Message message = new Message(); - - try { - message.getString(5); - fail("exception not thrown"); - } catch (final FieldNotFound e) { - } - - message.setString(5, "string5"); - - try { - assertEquals("string5", message.getString(5)); - } catch (final FieldNotFound e) { - fail("exception thrown"); - } - - try { - message.setString(100, null); - fail("exception not thrown"); - } catch (final NullPointerException e) { - } - } - - @Test - public void testMessagesetGetBoolean() { - final Message message = new Message(); - - try { - message.getBoolean(7); - fail("exception not thrown"); - } catch (final FieldNotFound e) { - } - - message.setBoolean(7, true); - - try { - assertTrue(message.getBoolean(7)); - } catch (final FieldNotFound e) { - fail("exception thrown"); - } - } - - @Test - public void testMessageSetGetChar() { - final Message message = new Message(); - - try { - message.getChar(12); - fail("exception not thrown"); - } catch (final FieldNotFound e) { - } - - message.setChar(12, 'a'); - - try { - assertEquals('a', message.getChar(12)); - } catch (final FieldNotFound e) { - fail("exception thrown"); - } - } - - @Test - public void testMessageSetGetChars() throws FieldNotFound { - final Message message = new Message(); - - try { - message.getChars(18); - fail("exception not thrown"); - } catch (final FieldNotFound e) { - } - - message.setChars(18, 'a', 'b', '4'); - assertArrayEquals(new char[] {'a', 'b', '4'}, message.getChars(18)); - } - - @Test - public void testMessageSetGetCharsInvalidFormatException() throws FieldNotFound { - expectedException.expect(FieldException.class); - expectedException.expectMessage("invalid char array: [65, 32, 98, 32, 48, 53]"); - - final Message message = new Message(); - message.setString(123, "A b 05"); - message.getChars(123); - } - - @Test - public void testMessageSetGetInt() { - final Message message = new Message(); - - try { - message.getInt(56); - fail("exception not thrown"); - } catch (final FieldNotFound e) { - } - - message.setInt(56, 23); - - try { - assertEquals(23, message.getInt(56)); - } catch (final FieldNotFound e) { - fail("exception thrown"); - } - } - - @Test - public void testMessageSetGetDouble() { - final Message message = new Message(); - - try { - message.getDouble(9812); - fail("exception not thrown"); - } catch (final FieldNotFound e) { - } - - message.setDouble(9812, 12.3443); - - try { - assertEquals(12.3443, message.getDouble(9812), 1e-10); - } catch (final FieldNotFound e) { - fail("exception thrown"); - } - } - - @Test - public void testMessageSetGetUtcTimeStamp() { - final Message message = new Message(); - - try { - message.getUtcTimeStamp(8); - fail("exception not thrown"); - } catch (final FieldNotFound e) { - } - - final TimeZone timezone = TimeZone.getTimeZone("GMT+0"); - final Calendar calendar = Calendar.getInstance(timezone); - calendar.set(2002, 8, 6, 12, 34, 56); - calendar.set(Calendar.MILLISECOND, 0); - - final LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(calendar.getTimeInMillis()), ZoneOffset.UTC); - message.setUtcTimeStamp(8, time); - - try { - assertEquals(message.getUtcTimeStamp(8), time); - } catch (final FieldNotFound e) { - fail("exception thrown"); - } - } - - @Test - public void testRemoveField() { - final Message message = new Message(); - message.setField(new StringField(12, "value")); - assertTrue(message.isSetField(12)); - message.removeField(12); - assertTrue(!message.isSetField(12)); - } - - @Test - public void testMessageIterator() { - Message message = new Message(); - java.util.Iterator> i = message.iterator(); - assertFalse(i.hasNext()); - try { - assertNull(i.next()); - fail("exception not thrown"); - } catch (final java.util.NoSuchElementException e) { - } - - try { - message = new Message("8=FIX.4.2\0019=12\00135=A\001108=30\00110=026\001"); - i = message.iterator(); - assertTrue(i.hasNext()); - StringField field = (StringField) i.next(); - assertEquals(108, field.getField()); - assertEquals("30", field.getValue()); - - assertFalse(i.hasNext()); - try { - assertNull(i.next()); - fail("exception not thrown"); - } catch (final java.util.NoSuchElementException e) { - } - - final java.util.Iterator> j = message.getHeader().iterator(); - assertTrue(j.hasNext()); - field = (StringField) j.next(); - assertEquals(8, field.getField()); - assertEquals("FIX.4.2", field.getValue()); - field = (StringField) j.next(); - assertEquals(9, field.getField()); - assertEquals("12", field.getValue()); - field = (StringField) j.next(); - assertEquals(35, field.getField()); - assertEquals("A", field.getValue()); - - assertFalse(j.hasNext()); - try { - assertNull(j.next()); - fail("exception not thrown"); - } catch (final java.util.NoSuchElementException e) { - } - } catch (final InvalidMessage e) { - fail("exception thrown"); - } - } - - @Test - public void testIsAdmin() { - final Message message = new Message(); - - message.getHeader().setString(MsgType.FIELD, MsgType.HEARTBEAT); - assertTrue(message.isAdmin()); - - message.getHeader().setString(MsgType.FIELD, MsgType.LOGON); - assertTrue(message.isAdmin()); - - message.getHeader().setString(MsgType.FIELD, MsgType.LOGOUT); - assertTrue(message.isAdmin()); - - message.getHeader().setString(MsgType.FIELD, MsgType.SEQUENCE_RESET); - assertTrue(message.isAdmin()); - - message.getHeader().setString(MsgType.FIELD, MsgType.RESEND_REQUEST); - assertTrue(message.isAdmin()); - - message.getHeader().setString(MsgType.FIELD, MsgType.TEST_REQUEST); - assertTrue(message.isAdmin()); - - message.getHeader().setString(MsgType.FIELD, MsgType.REJECT); - assertTrue(message.isAdmin()); - - message.getHeader().setString(MsgType.FIELD, MsgType.ORDER_SINGLE); - assertFalse(message.isAdmin()); - - message.getHeader().setString(MsgType.FIELD, MsgType.QUOTE_RESPONSE); - assertFalse(message.isAdmin()); - } - @Test public void testComponent() throws Exception { final Instrument instrument = new Instrument(); @@ -1317,91 +772,18 @@ public void testReplaceGroup() throws Exception { assertEquals("C", group.getField(clOrdID).getValue()); } - @Test - public void testFalseMessageStructureException() { - try { - final DataDictionary dd = DataDictionaryTest.getDictionary(); - // duplicated tag 98 - // QFJ-65 - new Message("8=FIX.4.4\0019=22\00135=A\00198=0\00198=0\001108=30\00110=223\001", dd, - true); - // For now, this will not cause an exception if the length and checksum are correct - } catch (final Exception e) { - final String text = e.getMessage(); - assertTrue("Wrong exception message: " + text, !text.contains("Actual body length")); - } - } - - @Test - public void testComponentInGroup() { - try { - final DataDictionary dd = DataDictionaryTest.getDictionary(); - // duplicated tag 98 - // QFJ-65 - // 8=FIX.4.4\0019=941\00135=AE\00149=ICE\00134=63\00152=20091117-18:59:04.780\00156=XXXX\001 - // 57=X\001571=219449\001487=0\001856=0\001828=0\001150=F\00117=44750544433\00139=2\001 - // 570=N\00155=480120\00148=WBS FMG0010-BRN FMG0010\00122=8\001461=FXXXXX\001916=20100201\001 - // 917=20100228\00132=1.0\00131=0.69\0019018=1\0019022=1\00175=20091117\00160=20091117-18:59:04.775\001 - // 552=1\00154=2\00137=41296064\00111=557859232\001453=7\001448=trader\001447=D\001452=11\001 - // 448=Trading Corp\001447=D\001452=13\001448=2757\001447=D\001452=56\001448=805\001447=D\001 - // 452=4\001448=11122556\001447=D\001452=51\001448=FCM\001447=D\001452=60\001448=U\001447=D\001 - // 452=5 4\00158=41293051\001555=2\001600=460130\001602=WBS FMG0010!\001603=8\001608=FXXXXX\001 - // 624=2\001637=80.78\001687=1.0\001654=41296074\0019019=1\0019023=1\0019020=20100201\001 - // 9021=20100228\001539=4\001524=805\001525=D\001538=4\001524=11122556\001525=D\001538=51\001 - // 524=FCM\001525=D\001538=60 524=U\001525=D\001538=54\001600=217927\001 - // 602=BRN FMG0010! 63=8 608-FXXXXX 624=1 637=80.09 687=1.0 654=41296073 9019=1 9023=1 9020=20100201 9021=20100228 539=4 524=805\001 - // 525=D\001538=4\001524=11122556 525=D\001538=51 524=Newedge 525=D 538=60 524=U 525=D 538=54 10=112 - new Message( - "8=FIX.4.4\0019=941\00135=AE\00149=ICE\00134=63\00152=20091117-18:59:04.780\00156=XXXX\001" + - "57=X\001571=219449\001487=0\001856=0\001828=0\001150=F\00117=44750544433\00139=2\001" + - "570=N\00155=480120\00148=WBS FMG0010-BRN FMG0010\00122=8\001461=FXXXXX\001916=20100201\001" + - "917=20100228\00132=1.0\00131=0.69\0019018=1\0019022=1\00175=20091117\00160=20091117-18:59:04.775\001" + - "552=1\00154=2\00137=41296064\00111=557859232\001453=7\001448=trader\001447=D\001452=11\001" + - "448=Trading Corp\001447=D\001452=13\001448=2757\001447=D\001452=56\001448=805\001447=D\001" + - "452=4\001448=11122556\001447=D\001452=51\001448=FCM\001447=D\001452=60\001448=U\001447=D\001" + - "452=5 4\00158=41293051\001555=2\001600=460130\001602=WBS FMG0010!\001603=8\001608=FXXXXX\001" + - "624=2\001637=80.78\001687=1.0\001654=41296074\0019019=1\0019023=1\0019020=20100201\001" + - "9021=20100228\001539=4\001524=805\001525=D\001538=4\001524=11122556\001525=D\001538=51\001" + - "524=FCM\001525=D\001538=60 524=U\001525=D\001538=54\001600=217927\001602=BRN FMG0010!\001" + - "63=8 608-FXXXXX\001624=1\001637=80.09\001687=1.0\001654=41296073\0019019=1\0019023=1\001" + - "9020=20100201\001021=20100228\001539=4\001524=805\001525=D\001538=4\001524=11122556\001" + - "525=D\001538=51\001524=FCM\001525=D\001538=60 524=U\001525=D\001538=54\001600=217927\001" + - "602=BRN FMG0010!\00163=8 608-FXXXXX\001624=1\001637=80.09\001687=1.0\001654=41296073\001" + - "9019=1\0019023=1\0019020=20100201\001021=20100228\001", - dd, true); - // For now, this will not cause an exception if the length and checksum are correct - } catch (final Exception e) { - final String text = e.getMessage(); - assertTrue("Wrong exception message: " + text, !text.contains("Actual body length")); - } - } - - @Test - public void testFalseMessageStructureException2() { - try { - final DataDictionary dd = DataDictionaryTest.getDictionary(); - // duplicated raw data length - // QFJ-121 - new Message("8=FIX.4.4\0019=22\00135=A\00196=X\001108=30\00110=223\001", dd, true); - } catch (final Exception e) { - final String text = e.getMessage(); - assertTrue("Wrong exception message: " + text, - text != null && !text.contains("Actual body length")); - } - } - @Test public void testFieldWithEqualsCharacter() { try { final DataDictionary dd = DataDictionaryTest.getDictionary(); final Message m = new Message( - "8=FIXT.1.1\0019=369\00135=W\00149=I\00156=F\00134=4\00152=20111021-15:09:16.535\001" + - "262=1319209757316210\00121=2\00155=EUR/USD\001461=RCSXX=0\001268=8\001" + - "269=0\001270=1.38898\001271=2000000\001269=0\001270=1.38897\001271=8000000\001" + - "269=0\001270=1.38854\001271=2000000\001269=1\001270=1.38855\001271=6000000\001" + - "269=1\001270=1.38856\001271=7000000\001269=1\001270=1.38857\001271=3000000\001" + - "269=1\001270=1.38858\001271=9000000\001269=1\001270=1.38859\001271=100000000\00110=51\001", - dd, true); + "8=FIXT.1.1\0019=369\00135=W\00149=I\00156=F\00134=4\00152=20111021-15:09:16.535\001" + + "262=1319209757316210\00121=2\00155=EUR/USD\001461=RCSXX=0\001268=8\001" + + "269=0\001270=1.38898\001271=2000000\001269=0\001270=1.38897\001271=8000000\001" + + "269=0\001270=1.38854\001271=2000000\001269=1\001270=1.38855\001271=6000000\001" + + "269=1\001270=1.38856\001271=7000000\001269=1\001270=1.38857\001271=3000000\001" + + "269=1\001270=1.38858\001271=9000000\001269=1\001270=1.38859\001271=100000000\00110=51\001", + dd, true); assertEquals(m.getString(461), "RCSXX=0"); final MarketDataSnapshotFullRefresh.NoMDEntries group = new MarketDataSnapshotFullRefresh.NoMDEntries(); m.getGroup(1, group); @@ -1420,13 +802,13 @@ public void testMiscFeeType() { try { final DataDictionary dd = DataDictionaryTest.getDictionary(); final Message m = new Message( - "8=FIXT.1.1\0019=369\00135=W\00149=I\00156=F\00134=4\00152=20111021-15:09:16.535\001" + - "262=1319209757316210\00121=2\00155=EUR/USD\001461=RCSXX=0\001268=8\001" + - "269=0\001270=1.38898\001271=2000000\001269=0\001270=1.38897\001271=8000000\001" + - "269=0\001270=1.38854\001271=2000000\001269=1\001270=1.38855\001271=6000000\001" + - "269=1\001270=1.38856\001271=7000000\001269=1\001270=1.38857\001271=3000000\001" + - "269=1\001270=1.38858\001271=9000000\001269=1\001270=1.38859\001271=100000000\00110=51\001", - dd, true); + "8=FIXT.1.1\0019=369\00135=W\00149=I\00156=F\00134=4\00152=20111021-15:09:16.535\001" + + "262=1319209757316210\00121=2\00155=EUR/USD\001461=RCSXX=0\001268=8\001" + + "269=0\001270=1.38898\001271=2000000\001269=0\001270=1.38897\001271=8000000\001" + + "269=0\001270=1.38854\001271=2000000\001269=1\001270=1.38855\001271=6000000\001" + + "269=1\001270=1.38856\001271=7000000\001269=1\001270=1.38857\001271=3000000\001" + + "269=1\001270=1.38858\001271=9000000\001269=1\001270=1.38859\001271=100000000\00110=51\001", + dd, true); assertEquals(m.getString(461), "RCSXX=0"); final MarketDataSnapshotFullRefresh.NoMDEntries group = new MarketDataSnapshotFullRefresh.NoMDEntries(); m.getGroup(1, group); @@ -1440,16 +822,6 @@ public void testMiscFeeType() { } } - /** - * Verify that an empty message can still be "printed" and doesn't result in any exceptions - */ - @Test - public void testEmptyMessageToString() throws Exception { - final Message msg = new quickfix.Message(); - assertNotNull(msg.toString()); - assertTrue("empty message contains no checksum", msg.toString().length() > 0); - } - @Test public void testMessageBytesField() throws Exception { final Logon logon = new Logon(); @@ -1531,7 +903,7 @@ public void testRepeatingGroupCount() throws Exception { // 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)); @@ -1628,7 +1000,7 @@ public void testUnknownFieldsInRepeatingGroupsAndValidation() throws Exception { failingTag = e.getField(); } assertEquals(Text.FIELD, failingTag); - + // but without checking for unknown message fields, validation should succeed dictionary.setAllowUnknownMessageFields(true); dictionary.validate(parsed2); @@ -1642,7 +1014,7 @@ public void testUnknownFieldsInRepeatingGroupsAndValidation() throws Exception { // QFJ-169 public void testInvalidFieldInGroup() throws Exception { SecurityRequestResult resultCode = new SecurityRequestResult( - SecurityRequestResult.NO_INSTRUMENTS_FOUND_THAT_MATCH_SELECTION_CRITERIA); + SecurityRequestResult.NO_INSTRUMENTS_FOUND); UnderlyingSymbol underlyingSymbolField = new UnderlyingSymbol("UND"); SecurityReqID id = new SecurityReqID("1234"); @@ -1669,7 +1041,7 @@ public void testInvalidFieldInGroup() throws Exception { responseMessage.setField(resultCode); DataDictionary dd = new DataDictionary(DataDictionaryTest.getDictionary()); - + int tagNo = 0; try { dd.validate(responseMessage, true); @@ -1704,8 +1076,8 @@ public void testInvalidFieldInGroup() throws Exception { @Test // QFJ-169/QFJ-791 public void testNestedRepeatingGroup() - throws Exception { - + 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|" @@ -1793,8 +1165,8 @@ public void testRepeatingGroupCountForIncorrectFieldOrder() throws Exception { private void testRepeatingGroupCountForFieldOrder(int fieldOrder[]) throws Exception { /* - * Prepare a very simple TradeCaptureReport message template with 1 - * repeating group. + * Prepare a very simple TradeCaptureReport message template with 1 + * repeating group. */ Message tcr = new TradeCaptureReport(); tcr.getHeader().setField(new MsgSeqNum(1)); @@ -1814,8 +1186,8 @@ private void testRepeatingGroupCountForFieldOrder(int fieldOrder[]) throws Excep 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. + * 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()); @@ -1835,7 +1207,7 @@ private void testRepeatingGroupCountForFieldOrder(int fieldOrder[]) throws Excep // 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 { @@ -1844,7 +1216,7 @@ public void testRepeatingGroupCountWithNonIntegerValues() throws Exception { ioi.setString(quickfix.field.NoPartyIDs.FIELD, "abc"); final String invalidCountMessage = ioi.toString(); try { - Message message = new Message(invalidCountMessage, dictionary); + Message message = new Message(invalidCountMessage, dictionary); } catch (final InvalidMessage im) { assertNotNull("InvalidMessage correctly thrown", im); } catch (final Throwable e) { @@ -1852,40 +1224,6 @@ public void testRepeatingGroupCountWithNonIntegerValues() throws Exception { 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", "\\|")); - } // QFJ-722 @Test @@ -1903,32 +1241,194 @@ public void testIfMessageHeaderIsOverwritten() { assertEquals(quickfix.fixt11.Message.Header.class, fixt11Message.getHeader().getClass()); } - // QFJ-722 @Test - public void testIfMessageHeaderIsCreatedWithEveryConstructor() throws Exception { - final String rawMessage = "8=FIX.4.2\0019=12\00135=A\001108=30\00110=026\001"; - final DataDictionary dataDictionary = new DataDictionary(DataDictionaryTest.getDictionary()); - - final Message emptyConstructor = new Message(); - assertNotNull(emptyConstructor.getHeader()); + public void shouldConvertToXmlWhenDataDictionaryLoadedWithExternalDTD() throws ConfigError { + DataDictionary dataDictionary = new DataDictionary("FIX_External_DTD.xml", DocumentBuilderFactory::newInstance); - final Message secondConstructor = new Message(new int[] {}); - assertNotNull(secondConstructor.getHeader()); + Message message = new Message(); + message.setString(Account.FIELD, "test-account"); - final Message thirdConstructor = new Message(rawMessage); - assertNotNull(thirdConstructor.getHeader()); + String xml = message.toXML(dataDictionary); + xml = xml.replace("\r", "").replace("\n", "").replaceAll(">\\s+<", "><"); + assertEquals("
", xml); + } + @Test + public void shouldConvertToXMLWithoutIndent() { + Message message = new Message(); + message.setString(Account.FIELD, "test-account"); + assertEquals("
", message.toXML()); + } + + @Test + public void shouldConvertToXMLWithIndent() { + Message message = new Message(); + message.setString(Account.FIELD, "test-account"); - final Message fourthConstructor = new Message(rawMessage, false); - assertNotNull(fourthConstructor.getHeader()); + String xml = message.toXML(true); + xml = xml.replace("\r", ""); + // formatting CDATA elements can be different across JVM's so we have to strip whitespaces before and after for the test to pass + // https://bugs.openjdk.java.net/browse/JDK-8215543 + xml = xml.replaceAll("\\s+\\s+", ""); + assertEquals("\n" + "\n" + + "
\n" + " \n" + + " \n" + " \n" + + " \n" + "\n", xml); + } - final Message fifthConstructor = new Message(rawMessage, dataDictionary); - assertNotNull(fifthConstructor.getHeader()); + @Test + public void testValidateFieldsOutOfOrderFIXT11() throws Exception { + final DataDictionary sessDictionary = DataDictionaryTest.getDictionary("FIXT11.xml"); + final DataDictionary appDictionary = DataDictionaryTest.getDictionary("FIX50SP2.xml"); + assertNotNull(sessDictionary); + assertNotNull(appDictionary); + assertNotEquals(appDictionary.getVersion(), sessDictionary.getVersion()); + + final String orderedData = "8=FIXT.1.1\u00019=561\u000135=AE\u0001" + + "34=545\u000149=SENDER\u000152=20220210-02:44:00.820\u000156=TARGET\u0001115=ON_BHEHALF\u0001" + + "1128=9\u0001" + + "627=2\u0001" + + "628=HOPID1\u0001629=20220414-15:22:54\u0001" + + "628=HOPID2\u0001629=20220414-15:22:54\u0001" + + "15=AUD\u000122=4\u000131=27\u000132=5000.000000000000\u000148=AU000000ANZ3\u000155=ANZ\u000160=20220210-02:43:27.796\u000164=20220214\u000175=20220210\u0001106=4075\u0001167=CS\u0001381=135000\u0001461=Exxxxx\u0001487=0\u0001762=1\u0001880=7533509260093686098:0#NORMAL#1644451200000000000\u00011003=1120000338\u00011015=0\u00011301=XASX\u0001" + + "552=2\u0001" + + "54=1\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u000111=7533509260093758035\u0001" + + "54=2\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u000111=7533509260093757876\u0001" + + "10=129\u0001"; + final TradeCaptureReport tcrOrdered = new TradeCaptureReport(); + tcrOrdered.fromString(orderedData, sessDictionary, appDictionary, true); + DataDictionary.validate(tcrOrdered, sessDictionary, appDictionary); + // As this is our reference message created with all validations switched on, make sure some message components + // are as expected + assertEquals(tcrOrdered.getHeader().getGroupCount(NoHops.FIELD), 2); + assertEquals(tcrOrdered.getGroupCount(NoSides.FIELD), 2); + + sessDictionary.setCheckFieldsOutOfOrder(false); + appDictionary.setCheckFieldsOutOfOrder(false); + + String unorderedData = "8=FIXT.1.1\u00019=561\u000135=AE\u0001" + + "15=AUD\u000122=4\u000131=27\u000132=5000.000000000000\u000148=AU000000ANZ3\u000155=ANZ\u000160=20220210-02:43:27.796\u000164=20220214\u000175=20220210\u0001106=4075\u0001167=CS\u0001381=135000\u0001461=Exxxxx\u0001487=0\u0001762=1\u0001880=7533509260093686098:0#NORMAL#1644451200000000000\u00011003=1120000338\u00011015=0\u00011301=XASX\u0001" + + "552=2\u0001" + + "54=1\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u000111=7533509260093758035\u0001" + + "54=2\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u000111=7533509260093757876\u0001" + + // Repeating Header Group, found just after a Repeating group within the body + "627=2\u0001628=HOPID1\u0001629=20220414-15:22:54\u0001628=HOPID2\u0001629=20220414-15:22:54\u0001" + + "34=545\u000149=SENDER\u000152=20220210-02:44:00.820\u000156=TARGET\u0001115=ON_BHEHALF\u00011128=9\u0001" + + "10=129\u0001"; + TradeCaptureReport tcrUnOrdered = new TradeCaptureReport(); + tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary); + + assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); + + unorderedData = "8=FIXT.1.1\u00019=561\u000135=AE\u0001" + + "15=AUD\u000122=4\u000131=27\u000132=5000.000000000000\u000148=AU000000ANZ3\u000155=ANZ\u000160=20220210-02:43:27.796\u000164=20220214\u000175=20220210\u0001106=4075\u0001167=CS\u0001381=135000\u0001461=Exxxxx\u0001487=0\u0001762=1\u0001880=7533509260093686098:0#NORMAL#1644451200000000000\u00011003=1120000338\u00011015=0\u00011301=XASX\u0001" + + "552=2\u0001" + + "54=1\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u000111=7533509260093758035\u0001" + + "54=2\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u000111=7533509260093757876\u0001" + + // Header tag found just after Repeating group within the body + "34=545\u000149=SENDER\u000152=20220210-02:44:00.820\u000156=TARGET\u0001115=ON_BHEHALF\u00011128=9\u0001" + + "627=2\u0001628=HOPID1\u0001629=20220414-15:22:54\u0001628=HOPID2\u0001629=20220414-15:22:54\u0001" + + "10=129\u0001"; + tcrUnOrdered = new TradeCaptureReport(); + tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary); + + assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); + + unorderedData = "8=FIXT.1.1\u00019=561\u000135=AE\u0001" + + "15=AUD\u000122=4\u000131=27\u000132=5000.000000000000\u000148=AU000000ANZ3\u000155=ANZ\u000160=20220210-02:43:27.796\u000164=20220214\u000175=20220210\u0001106=4075\u0001167=CS\u0001381=135000\u0001461=Exxxxx\u0001487=0\u0001762=1\u0001880=7533509260093686098:0#NORMAL#1644451200000000000\u00011003=1120000338\u00011015=0\u00011301=XASX\u0001" + + // Some Header fields found after body fields detected + "34=545\u000149=SENDER\u000152=20220210-02:44:00.820\u000156=TARGET\u0001115=ON_BHEHALF\u00011128=9\u0001" + + // Repeating Group + "552=2\u0001" + + "54=1\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u000111=7533509260093758035\u0001" + + "54=2\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u000111=7533509260093757876\u0001" + + // Some repeating Header tags + "627=2\u0001628=HOPID1\u0001629=20220414-15:22:54\u0001628=HOPID2\u0001629=20220414-15:22:54\u0001" + + "10=129\u0001"; + tcrUnOrdered = new TradeCaptureReport(); + tcrUnOrdered.fromString(unorderedData, sessDictionary, appDictionary, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, appDictionary); + + assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); + + } + + @Test + public void testValidateFieldsOutOfOrderPreFIXT11() throws Exception { + final DataDictionary sessDictionary = DataDictionaryTest.getDictionary("FIX44.xml"); + assertNotNull(sessDictionary); - final Message sixthConstructor = new Message(rawMessage, dataDictionary, false); - assertNotNull(sixthConstructor.getHeader()); + final String orderedData = + "8=FIX.4.4\u00019=551\u000135=AE\u0001" + + "34=545\u000149=SENDER\u000152=20220210-02:44:00.820\u000156=TARGET\u0001115=ON_BHEHALF\u0001" + + "627=2\u0001628=HOPID1\u0001629=20220414-15:22:54\u0001628=HOPID2\u0001629=20220414-15:22:54\u0001" + + "22=4\u000131=27\u000132=5000.000000000000\u000148=AU000000ANZ3\u000155=ANZ\u000160=20220210-02:43:27.796\u000164=20220214\u000175=20220210\u0001106=4075\u0001167=CS\u0001461=Exxxxx\u0001487=0\u0001570=N\u0001571=TradeReportID\u0001762=1\u0001880=7533509260093686098:0#NORMAL#1644451200000000000\u0001" + + "552=2\u0001" + + "54=1\u000137=OrderID1\u000111=7533509260093758035\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u0001" + + "54=2\u000137=OrderID2\u000111=7533509260093757876\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u0001" + + "10=191\u0001"; + final TradeCaptureReport tcrOrdered = new TradeCaptureReport(); + tcrOrdered.fromString(orderedData, sessDictionary, true); + DataDictionary.validate(tcrOrdered, sessDictionary, sessDictionary); + + // As this is our reference message created with all validations switched on, + // make sure some message components + // are as expected + assertEquals(tcrOrdered.getHeader().getGroupCount(NoHops.FIELD), 2); + assertEquals(tcrOrdered.getGroupCount(NoSides.FIELD), 2); + + sessDictionary.setCheckFieldsOutOfOrder(false); + + String unorderedData = "8=FIX.4.4\u00019=551\u000135=AE\u0001" + + "22=4\u000131=27\u000132=5000.000000000000\u000148=AU000000ANZ3\u000155=ANZ\u000160=20220210-02:43:27.796\u000164=20220214\u000175=20220210\u0001106=4075\u0001167=CS\u0001461=Exxxxx\u0001487=0\u0001570=N\u0001571=TradeReportID\u0001762=1\u0001880=7533509260093686098:0#NORMAL#1644451200000000000\u0001" + + "552=2\u0001" + + "54=1\u000137=OrderID1\u000111=7533509260093758035\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u0001" + + "54=2\u000137=OrderID2\u000111=7533509260093757876\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u0001" + // Repeating Header Group, found just after a Repeating group within the body + + "627=2\u0001628=HOPID1\u0001629=20220414-15:22:54\u0001628=HOPID2\u0001629=20220414-15:22:54\u0001" + + "34=545\u000149=SENDER\u000152=20220210-02:44:00.820\u000156=TARGET\u0001115=ON_BHEHALF\u0001" + + "10=191\u0001"; + TradeCaptureReport tcrUnOrdered = new TradeCaptureReport(); + tcrUnOrdered.fromString(unorderedData, sessDictionary, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary); + + assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); + + unorderedData = "8=FIX.4.4\u00019=551\u000135=AE\u0001" + + "22=4\u000131=27\u000132=5000.000000000000\u000148=AU000000ANZ3\u000155=ANZ\u000160=20220210-02:43:27.796\u000164=20220214\u000175=20220210\u0001106=4075\u0001167=CS\u0001461=Exxxxx\u0001487=0\u0001570=N\u0001571=TradeReportID\u0001762=1\u0001880=7533509260093686098:0#NORMAL#1644451200000000000\u0001" + + "552=2\u0001" + + "54=1\u000137=OrderID1\u000111=7533509260093758035\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u0001" + + "54=2\u000137=OrderID2\u000111=7533509260093757876\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u0001" + // Header tag found just after Repeating group within the body + + "34=545\u000149=SENDER\u000152=20220210-02:44:00.820\u000156=TARGET\u0001115=ON_BHEHALF\u0001" + + "627=2\u0001628=HOPID1\u0001629=20220414-15:22:54\u0001628=HOPID2\u0001629=20220414-15:22:54\u0001" + + + "10=191\u0001"; + tcrUnOrdered = new TradeCaptureReport(); + tcrUnOrdered.fromString(unorderedData, sessDictionary, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary); + + assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); + + unorderedData = "8=FIX.4.4\u00019=551\u000135=AE\u0001" + // Some body tags + + "22=4\u000131=27\u000132=5000.000000000000\u000148=AU000000ANZ3\u000155=ANZ\u000160=20220210-02:43:27.796\u000164=20220214\u000175=20220210\u0001106=4075\u0001167=CS\u0001461=Exxxxx\u0001487=0\u0001570=N\u0001571=TradeReportID\u0001762=1\u0001880=7533509260093686098:0#NORMAL#1644451200000000000\u0001" + // The some Header tags + + "34=545\u000149=SENDER\u000152=20220210-02:44:00.820\u000156=TARGET\u0001115=ON_BHEHALF\u0001" + // A Repeating body group + + "552=2\u0001" + + "54=1\u000137=OrderID1\u000111=7533509260093758035\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u0001" + + "54=2\u000137=OrderID2\u000111=7533509260093757876\u0001453=1\u0001448=338-3\u0001447=D\u0001452=1\u00011=1040445\u0001576=1\u0001577=0\u0001" + // Repeating Header Group + + "627=2\u0001628=HOPID1\u0001629=20220414-15:22:54\u0001628=HOPID2\u0001629=20220414-15:22:54\u0001" + + "10=191\u0001"; + tcrUnOrdered = new TradeCaptureReport(); + tcrUnOrdered.fromString(unorderedData, sessDictionary, true); + DataDictionary.validate(tcrUnOrdered, sessDictionary, sessDictionary); + + assertEquals(tcrOrdered.toString(), tcrUnOrdered.toString()); - final Message seventhConstructor = new Message(rawMessage, dataDictionary, dataDictionary, false); - assertNotNull(seventhConstructor.getHeader()); } private void assertHeaderField(Message message, String expectedValue, int field) diff --git a/quickfixj-core/src/test/java/quickfix/MessageUtilsTest.java b/quickfixj-core/src/test/java/quickfix/MessageUtilsTest.java index 8e4e6fb88f..803b9d6d5e 100644 --- a/quickfixj-core/src/test/java/quickfix/MessageUtilsTest.java +++ b/quickfixj-core/src/test/java/quickfix/MessageUtilsTest.java @@ -19,39 +19,38 @@ package quickfix; -import junit.framework.TestCase; import quickfix.field.ApplVerID; -import quickfix.field.BeginString; import quickfix.field.DefaultApplVerID; import quickfix.field.EmailThreadID; import quickfix.field.EmailType; import quickfix.field.EncryptMethod; import quickfix.field.HeartBtInt; -import quickfix.field.MsgType; import quickfix.field.SenderCompID; import quickfix.field.Subject; import quickfix.field.TargetCompID; import quickfix.fix40.Logon; import quickfix.fix50.Email; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.stub; import static org.mockito.Mockito.when; -public class MessageUtilsTest extends TestCase { - - public void testGetStringField() throws Exception { - String messageString = "8=FIX.4.2\0019=12\00135=X\001108=30\00110=049\001"; - assertEquals("wrong value", "FIX.4.2", MessageUtils.getStringField(messageString, - BeginString.FIELD)); - assertEquals("wrong value", "X", MessageUtils.getStringField(messageString, MsgType.FIELD)); - assertNull(messageString, MessageUtils.getStringField(messageString, SenderCompID.FIELD)); - } +/** + * NOTE: There are two MessageUtilsTests. + * One in quickfixj-base, one in quickfixj-core, which each test + * some functionality. This test covers some test cases that cannot + * be tested in the quickfixj-base module due to classes that are + * generated later in the compile process, e.g. MessageFactories. + */ +public class MessageUtilsTest { + @Test public void testSessionIdFromMessage() throws Exception { Message message = new Logon(); message.getHeader().setString(SenderCompID.FIELD, "TW"); @@ -62,6 +61,7 @@ public void testSessionIdFromMessage() throws Exception { assertEquals("ISLD", sessionID.getTargetCompID()); } + @Test public void testReverseSessionIdFromMessage() throws Exception { Message message = new Logon(); message.getHeader().setString(SenderCompID.FIELD, "TW"); @@ -72,6 +72,7 @@ public void testReverseSessionIdFromMessage() throws Exception { assertEquals("TW", sessionID.getTargetCompID()); } + @Test public void testReverseSessionIdFromMessageWithMissingFields() throws Exception { Message message = new Logon(); SessionID sessionID = MessageUtils.getReverseSessionID(message); @@ -80,78 +81,21 @@ public void testReverseSessionIdFromMessageWithMissingFields() throws Exception assertEquals(sessionID.getTargetCompID(), SessionID.NOT_SET); } - public void testSessionIdFromRawMessage() throws Exception { - String messageString = "8=FIX.4.0\0019=56\00135=A\00134=1\00149=TW\001" + - "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; - SessionID sessionID = MessageUtils.getSessionID(messageString); - assertEquals(sessionID.getBeginString(), "FIX.4.0"); - assertEquals("TW", sessionID.getSenderCompID()); - assertEquals("ISLD", sessionID.getTargetCompID()); - } - - public void testReverseSessionIdFromRawMessage() throws Exception { - String messageString = "8=FIX.4.0\0019=56\00135=A\00134=1\00149=TW\00150=TWS\001" + - "142=TWL\00152=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; - SessionID sessionID = MessageUtils.getReverseSessionID(messageString); - assertEquals(sessionID.getBeginString(), "FIX.4.0"); - assertEquals("ISLD", sessionID.getSenderCompID()); - assertEquals("TW", sessionID.getTargetCompID()); - assertEquals("TWS", sessionID.getTargetSubID()); - assertEquals("TWL", sessionID.getTargetLocationID()); - } - - public void testMessageType() throws Exception { - String messageString = "8=FIX.4.0\0019=56\00135=A\00134=1\00149=TW\001" + - "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; - assertEquals("A", MessageUtils.getMessageType(messageString)); - } - - public void testMessageTypeError() throws Exception { - String messageString = "8=FIX.4.0\0019=56\00134=1\00149=TW\001" + - "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; - try { - MessageUtils.getMessageType(messageString); - fail("expected exception"); - } catch (InvalidMessage e) { - // expected - } - } - - public void testMessageTypeError2() throws Exception { - String messageString = "8=FIX.4.0\0019=56\00135=1"; - try { - MessageUtils.getMessageType(messageString); - fail("expected exception"); - } catch (InvalidMessage e) { - // expected - } - } - - public void testGetNonexistentStringField() throws Exception { - String messageString = "8=FIX.4.0\0019=56\00134=1\00149=TW\001" + - "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; - assertNull(MessageUtils.getStringField(messageString, 35)); - } - - public void testGetStringFieldWithBadValue() throws Exception { - String messageString = "8=FIX.4.0\0019=56\00134=1\00149=TW\001" + - "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223"; - assertNull(MessageUtils.getStringField(messageString, 10)); - } - + @Test public void testParse() throws Exception { Session mockSession = mock(Session.class); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); - stub(mockSession.getDataDictionaryProvider()).toReturn(mockDataDictionaryProvider); - stub(mockSession.getMessageFactory()).toReturn(new quickfix.fix40.MessageFactory()); + when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); + when(mockSession.getMessageFactory()).thenReturn(new quickfix.fix40.MessageFactory()); String messageString = "8=FIX.4.0\0019=56\00135=A\00134=1\00149=TW\001" + "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=223\001"; - Message message = MessageUtils.parse(mockSession, messageString); + Message message = MessageSessionUtils.parse(mockSession, messageString); - assertThat(message, is(notNullValue())); + assertThat(message, notNullValue()); } + @Test public void testLegacyParse() throws Exception { String data = "8=FIX.4.4\0019=309\00135=8\00149=ASX\00156=CL1_FIX44\00134=4\001" + "52=20060324-01:05:58\00117=X-B-WOW-1494E9A0:58BD3F9D-1109\001150=D\001" + @@ -161,59 +105,78 @@ public void testLegacyParse() throws Exception { "447=D\001452=4\001448=FIX11\001447=D\001452=36\00160=20060320-03:34:29\00110=169\001"; Message message = MessageUtils.parse(new quickfix.fix40.MessageFactory(), DataDictionaryTest.getDictionary(), data); - assertThat(message, is(notNullValue())); + assertThat(message, notNullValue()); } + @Test public void testParseFixt() throws Exception { Session mockSession = mock(Session.class); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); - stub(mockSession.getDataDictionaryProvider()).toReturn(mockDataDictionaryProvider); - stub(mockSession.getMessageFactory()).toReturn(new quickfix.fix40.MessageFactory()); + when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); + when(mockSession.getMessageFactory()).thenReturn(new quickfix.fix40.MessageFactory()); Email email = new Email(new EmailThreadID("THREAD_ID"), new EmailType(EmailType.NEW), new Subject("SUBJECT")); email.getHeader().setField(new ApplVerID(ApplVerID.FIX42)); email.getHeader().setField(new SenderCompID("SENDER")); email.getHeader().setField(new TargetCompID("TARGET")); - Message message = MessageUtils.parse(mockSession, email.toString()); + Message message = MessageSessionUtils.parse(mockSession, email.toString()); assertThat(message, is(notNullValue())); - assertThat(message, is(quickfix.fix40.Email.class)); + assertThat(message, instanceOf(quickfix.fix40.Email.class)); } + @Test public void testParseFixtLogon() throws Exception { Session mockSession = mock(Session.class); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); - stub(mockSession.getDataDictionaryProvider()).toReturn(mockDataDictionaryProvider); - stub(mockSession.getMessageFactory()).toReturn(new DefaultMessageFactory()); + when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); + when(mockSession.getMessageFactory()).thenReturn(new DefaultMessageFactory()); quickfix.fixt11.Logon logon = new quickfix.fixt11.Logon(new EncryptMethod(EncryptMethod.NONE_OTHER), new HeartBtInt(30), new DefaultApplVerID(ApplVerID.FIX42)); - Message message = MessageUtils.parse(mockSession, logon.toString()); + Message message = MessageSessionUtils.parse(mockSession, logon.toString()); assertThat(message, is(notNullValue())); - assertThat(message, is(quickfix.fixt11.Logon.class)); + assertThat(message, instanceOf(quickfix.fixt11.Logon.class)); } + @Test + public void testParseFixtLogout() throws Exception { + Session mockSession = mock(Session.class); + DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); + when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); + when(mockSession.getMessageFactory()).thenReturn(new DefaultMessageFactory()); + + quickfix.fixt11.Logout logout = new quickfix.fixt11.Logout(); + + Message message = MessageSessionUtils.parse(mockSession, logout.toString()); + + assertThat(message, is(notNullValue())); + assertThat(message, instanceOf(quickfix.fixt11.Logout.class)); + } + + @Test public void testParseFix50() throws Exception { Session mockSession = mock(Session.class); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); - stub(mockSession.getDataDictionaryProvider()).toReturn(mockDataDictionaryProvider); - stub(mockSession.getMessageFactory()).toReturn(new DefaultMessageFactory()); + when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); + when(mockSession.getMessageFactory()).thenReturn(new DefaultMessageFactory()); Email email = new Email(new EmailThreadID("THREAD_ID"), new EmailType(EmailType.NEW), new Subject("SUBJECT")); email.getHeader().setField(new ApplVerID(ApplVerID.FIX50)); email.getHeader().setField(new SenderCompID("SENDER")); email.getHeader().setField(new TargetCompID("TARGET")); - Message message = MessageUtils.parse(mockSession, email.toString()); + Message message = MessageSessionUtils.parse(mockSession, email.toString()); assertThat(message, is(notNullValue())); - assertThat(message, is(quickfix.fix50.Email.class)); + assertThat(message, instanceOf(quickfix.fix50.Email.class)); } // QFJ-973 + @Test public void testParseMessageWithoutChecksumValidation() throws InvalidMessage { Session mockSession = mock(Session.class); when(mockSession.isValidateChecksum()).thenReturn(Boolean.FALSE); @@ -221,15 +184,15 @@ public void testParseMessageWithoutChecksumValidation() throws InvalidMessage { DataDictionary dataDictionary = mock(DataDictionary.class); DataDictionaryProvider mockDataDictionaryProvider = mock(DataDictionaryProvider.class); when(mockDataDictionaryProvider.getSessionDataDictionary(any(String.class))).thenReturn(dataDictionary); - stub(mockSession.getDataDictionaryProvider()).toReturn(mockDataDictionaryProvider); - stub(mockSession.getMessageFactory()).toReturn(new quickfix.fix40.MessageFactory()); + when(mockSession.getDataDictionaryProvider()).thenReturn(mockDataDictionaryProvider); + when(mockSession.getMessageFactory()).thenReturn(new quickfix.fix40.MessageFactory()); String messageString = "8=FIX.4.0\0019=56\00135=A\00134=1\00149=TW\001" + "52=20060118-16:34:19\00156=ISLD\00198=0\001108=2\00110=283\001"; - Message message = MessageUtils.parse(mockSession, messageString); + Message message = MessageSessionUtils.parse(mockSession, messageString); - assertThat(message, is(notNullValue())); + assertThat(message, notNullValue()); } } diff --git a/quickfixj-core/src/test/java/quickfix/MockSystemTimeSource.java b/quickfixj-core/src/test/java/quickfix/MockSystemTimeSource.java index b092e9136e..3c9dea46c4 100644 --- a/quickfixj-core/src/test/java/quickfix/MockSystemTimeSource.java +++ b/quickfixj-core/src/test/java/quickfix/MockSystemTimeSource.java @@ -40,7 +40,7 @@ public void setSystemTimes(long[] times) { systemTimes = times; } - private void setSystemTimes(long time) { + void setSystemTimes(long time) { systemTimes = new long[] { time }; } diff --git a/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java b/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java index 8b7edfd417..b4c06797ae 100644 --- a/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java +++ b/quickfixj-core/src/test/java/quickfix/MultiAcceptorTest.java @@ -19,7 +19,6 @@ package quickfix; -import junit.framework.TestCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import quickfix.field.TestReqID; @@ -27,27 +26,42 @@ import quickfix.mina.ProtocolFactory; import java.util.HashMap; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - -public class MultiAcceptorTest extends TestCase { +import org.apache.mina.util.AvailablePortFinder; +import org.junit.After; +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 org.junit.Test; + +public class MultiAcceptorTest { private final Logger log = LoggerFactory.getLogger(getClass()); private TestAcceptorApplication testAcceptorApplication; - protected void tearDown() throws Exception { - super.tearDown(); + @After + public void tearDown() throws Exception { testAcceptorApplication.tearDown(); } + @Test public void testMultipleAcceptor() throws Exception { testAcceptorApplication = new TestAcceptorApplication(3); Acceptor acceptor = null; Initiator initiator = null; try { - acceptor = createAcceptor(); + int freePort1 = AvailablePortFinder.getNextAvailable(); + int freePort2 = AvailablePortFinder.getNextAvailable(); + int freePort3 = AvailablePortFinder.getNextAvailable(); + + acceptor = createAcceptor(freePort1, freePort2, freePort3, false); acceptor.start(); - initiator = createInitiator(false); + initiator = createInitiator(false, freePort1, freePort2, freePort3); initiator.start(); testAcceptorApplication.waitForLogon(); @@ -73,16 +87,21 @@ public void testMultipleAcceptor() throws Exception { } } + @Test public void testMessageSentOnWrongAcceptor() throws Exception { testAcceptorApplication = new TestAcceptorApplication(2); Acceptor acceptor = null; Initiator initiator = null; try { - acceptor = createAcceptor(); + int freePort1 = AvailablePortFinder.getNextAvailable(); + int freePort2 = AvailablePortFinder.getNextAvailable(); + int freePort3 = AvailablePortFinder.getNextAvailable(); + + acceptor = createAcceptor(freePort1, freePort2, freePort3, false); acceptor.start(); - initiator = createInitiator(true); + initiator = createInitiator(true, freePort1, freePort2, freePort3); initiator.start(); testAcceptorApplication.waitForLogon(); @@ -111,6 +130,28 @@ public void testMessageSentOnWrongAcceptor() throws Exception { } } + @Test + public void testContinueInitializationOnError() throws Exception { + testAcceptorApplication = new TestAcceptorApplication(3); + int freePort1 = AvailablePortFinder.getNextAvailable(); + int freePort2 = AvailablePortFinder.getNextAvailable(); + int freePort3 = AvailablePortFinder.getNextAvailable(); + + Acceptor acceptor = createAcceptor(freePort1, freePort2, freePort3, true); + acceptor.start(); + + List acceptorSessions = acceptor.getSessions(); + assertEquals(2, acceptorSessions.size()); + SessionID session1 = new SessionID(FixVersions.BEGINSTRING_FIX42, "ACCEPTOR-" + 1, "INITIATOR"); + SessionID session2 = new SessionID(FixVersions.BEGINSTRING_FIX42, "ACCEPTOR-" + 2, "INITIATOR"); + SessionID session3 = new SessionID(FixVersions.BEGINSTRING_FIX42, "ACCEPTOR-" + 3, "INITIATOR"); + assertTrue(acceptorSessions.contains(session1)); + assertFalse(acceptorSessions.contains(session2)); + assertTrue(acceptorSessions.contains(session3)); + + acceptor.stop(true); + } + private void doSessionDispatchingTest(int i) throws SessionNotFound, InterruptedException, FieldNotFound { TestRequest message = new TestRequest(); @@ -165,6 +206,7 @@ public void waitForLogon() { try { logonLatch.await(20, TimeUnit.SECONDS); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); fail(e.getMessage()); } } @@ -179,6 +221,7 @@ public synchronized void waitForMessages() { fail("Timed out waiting for message"); } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); fail(e.getMessage()); } } @@ -188,7 +231,7 @@ public void tearDown() { } } - private Initiator createInitiator(boolean wrongPort) throws ConfigError { + private Initiator createInitiator(boolean wrongPort, int freePort1, int freePort2, int freePort3) throws ConfigError { SessionSettings settings = new SessionSettings(); HashMap defaults = new HashMap<>(); defaults.put("ConnectionType", "initiator"); @@ -201,9 +244,9 @@ private Initiator createInitiator(boolean wrongPort) throws ConfigError { settings.setString("BeginString", FixVersions.BEGINSTRING_FIX42); settings.set(defaults); - configureInitiatorForSession(settings, 1, 10001); - configureInitiatorForSession(settings, 2, 10002); - configureInitiatorForSession(settings, 3, wrongPort ? 1000 : 10003); + configureInitiatorForSession(settings, 1, freePort1); + configureInitiatorForSession(settings, 2, freePort2); + configureInitiatorForSession(settings, 3, wrongPort ? 1000 : freePort3); MessageStoreFactory factory = new MemoryStoreFactory(); quickfix.LogFactory logFactory = new SLF4JLogFactory(new SessionSettings()); @@ -218,7 +261,7 @@ private void configureInitiatorForSession(SessionSettings settings, int i, int p settings.setString(sessionID, "SocketConnectPort", Integer.toString(port)); } - private Acceptor createAcceptor() throws ConfigError { + private Acceptor createAcceptor(int freePort1, int freePort2, int freePort3, boolean addFaultySession) throws ConfigError { SessionSettings settings = new SessionSettings(); HashMap defaults = new HashMap<>(); defaults.put("ConnectionType", "acceptor"); @@ -228,11 +271,22 @@ private Acceptor createAcceptor() throws ConfigError { defaults.put(SessionSettings.TARGETCOMPID, "TW"); defaults.put("BeginString", "FIX.4.2"); defaults.put("ResetOnDisconnect", "Y"); + if (addFaultySession) { + defaults.put("UseDataDictionary", "Y"); + defaults.put("DataDictionary", "FIX42.xml"); + defaults.put(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR, "Y"); + } settings.set(defaults); - configureAcceptorForSession(settings, 1, 10001); - configureAcceptorForSession(settings, 2, 10002); - configureAcceptorForSession(settings, 3, 10003); + configureAcceptorForSession(settings, 1, freePort1); + configureAcceptorForSession(settings, 2, freePort2); + configureAcceptorForSession(settings, 3, freePort3); + + if (addFaultySession) { + // set a non-existent dictionary to let one session creation fail + SessionID faultySessionID = new SessionID(FixVersions.BEGINSTRING_FIX42, "ACCEPTOR-" + 2, "INITIATOR"); + settings.setString(faultySessionID, "DataDictionary", "unknown.xml"); + } MessageStoreFactory factory = new MemoryStoreFactory(); quickfix.LogFactory logFactory = new SLF4JLogFactory(new SessionSettings()); diff --git a/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java b/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java index 1132a307f2..a6cf3ea396 100644 --- a/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java +++ b/quickfixj-core/src/test/java/quickfix/RepeatingGroupTest.java @@ -148,6 +148,25 @@ private quickfix.fix50sp2.QuoteRequest.NoRelatedSym buildNestedGroupWithStandard return gNoRelatedSym; } + private quickfix.fixlatest.QuoteRequest.NoRelatedSym buildNestedGroupWithStandardFieldsFIXLatest( + String settingValue) { + // The root group + final quickfix.fixlatest.QuoteRequest.NoRelatedSym gNoRelatedSym = new quickfix.fixlatest.QuoteRequest.NoRelatedSym(); + + // The nested group + final quickfix.fixlatest.QuoteRequest.NoRelatedSym.NoLegs nestedgroup = new quickfix.fixlatest.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.fixlatest.QuoteRequest.NoRelatedSym.NoLegs oneMoreNestedgroup = new quickfix.fixlatest.QuoteRequest.NoRelatedSym.NoLegs(); + oneMoreNestedgroup.setField(new LegSymbol("Donald")); + gNoRelatedSym.addGroup(oneMoreNestedgroup); + + return gNoRelatedSym; + } + @Test public void testSettingGettingNestedGroupWithStandardFields() throws FieldNotFound { final String settingValue = "SETTING_VALUE"; @@ -356,6 +375,32 @@ public void testValidationWithNestedGroupAndStandardFieldsFIX50SP2() throws Inva assertEquals(2, validatedMessage.getGroupCount(gNoRelatedSym.getFieldTag())); } + @Test + public void testValidationWithNestedGroupAndStandardFieldsFIXLatest() throws InvalidMessage, ConfigError { + final quickfix.fixlatest.QuoteRequest quoteRequest = new quickfix.fixlatest.QuoteRequest(); + + final quickfix.field.QuoteReqID gQuoteReqID = new quickfix.field.QuoteReqID(); + gQuoteReqID.setValue("12342"); + quoteRequest.setField(gQuoteReqID); + + final quickfix.fixlatest.QuoteRequest.NoRelatedSym gNoRelatedSym = buildNestedGroupWithStandardFieldsFIXLatest("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 fixDataDictionary = new DataDictionary("FIXLatest.xml"); + final quickfix.fixlatest.QuoteRequest validatedMessage = (quickfix.fixlatest.QuoteRequest) messageFactory.create(FixVersions.FIXLATEST, QuoteRequest.MSGTYPE); + validatedMessage.fromString(sourceFIXString, fixDataDictionary, true); + + String validateFIXString = validatedMessage.toString(); + + assertEquals("Message validation failed", sourceFIXString, validateFIXString); + assertEquals(2, validatedMessage.getGroupCount(gNoRelatedSym.getFieldTag())); + } + @Test public void testValidationWithNestedGroupAndStandardFieldsWithoutDelimiter() throws InvalidMessage { final quickfix.fix44.QuoteRequest quoteRequest = new quickfix.fix44.QuoteRequest(); diff --git a/quickfixj-core/src/test/java/quickfix/SessionDisconnectConcurrentlyTest.java b/quickfixj-core/src/test/java/quickfix/SessionDisconnectConcurrentlyTest.java index 019a6f0843..66d8edd9ba 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionDisconnectConcurrentlyTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionDisconnectConcurrentlyTest.java @@ -134,6 +134,7 @@ public void waitForLogon() { try { logonLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); fail(e.getMessage()); } } @@ -148,6 +149,7 @@ public synchronized void waitForMessages() { fail("Timed out waiting for message"); } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); fail(e.getMessage()); } } @@ -225,7 +227,7 @@ public void run() { try { Thread.sleep(12000); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); } threadIds = bean.findDeadlockedThreads(); } @@ -330,28 +332,37 @@ public void disconnect() { } } - public void onConnect() { + public void onConnect(SessionID sessionID) { } - public void onDisconnect() { + public void onDisconnect(SessionID sessionID) { } - public void onLogon() { + public void onLogon(SessionID sessionID) { } - public void onLogout() { + public void onLogout(SessionID sessionID) { } - public void onReset() { + public void onReset(SessionID sessionID) { } - public void onRefresh() { + public void onRefresh(SessionID sessionID) { } - public void onMissedHeartBeat() { + public void onMissedHeartBeat(SessionID sessionID) { } - public void onHeartBeatTimeout() { + public void onHeartBeatTimeout(SessionID sessionID) { + } + + public void onResendRequestSent(SessionID sessionID, int beginSeqNo, int endSeqNo, int currentEndSeqNo) { + } + + public void onSequenceResetReceived(SessionID sessionID, int newSeqNo, boolean gapFillFlag) { + } + + public void onResendRequestSatisfied(SessionID sessionID, int beginSeqNo, int endSeqNo) { } } diff --git a/quickfixj-core/src/test/java/quickfix/SessionFactoryTestSupport.java b/quickfixj-core/src/test/java/quickfix/SessionFactoryTestSupport.java index 7e12288cbf..35356fb9e5 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionFactoryTestSupport.java +++ b/quickfixj-core/src/test/java/quickfix/SessionFactoryTestSupport.java @@ -3,6 +3,8 @@ import quickfix.field.DefaultApplVerID; import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.function.Supplier; @@ -75,6 +77,7 @@ public static final class Builder { private Supplier sessionIDSupplier = () -> new SessionID(beginString, senderCompID, targetCompID); private Supplier applicationSupplier = UnitTestApplication::new; private Supplier messageStoreFactorySupplier = MemoryStoreFactory::new; + private Supplier messageQueueFactorySupplier = InMemoryMessageQueueFactory::new; private Supplier dataDictionaryProviderSupplier = () -> null; private Supplier sessionScheduleSupplier = () -> null; private Supplier logFactorySupplier = () -> new ScreenLogFactory(true, true, true); @@ -92,6 +95,7 @@ public static final class Builder { private boolean persistMessages = false; private final boolean useClosedRangeForResend = false; private final double testRequestDelayMultiplier = 1.5; + private final double heartBeatTimeoutMultiplier = 2.5; private DefaultApplVerID senderDefaultApplVerID = null; private boolean validateSequenceNumbers = true; private final int[] logonIntervals = new int[]{5}; @@ -108,10 +112,12 @@ public static final class Builder { private boolean enableNextExpectedMsgSeqNum = false; private final boolean enableLastMsgSeqNumProcessed = false; private final boolean validateChecksum = true; + private final boolean allowPosDup = false; + private List logonTags = new ArrayList<>(); public Session build() { - return new Session(applicationSupplier.get(), messageStoreFactorySupplier.get(), sessionIDSupplier.get(), - dataDictionaryProviderSupplier.get(), sessionScheduleSupplier.get(), logFactorySupplier.get(), + return new Session(applicationSupplier.get(), messageStoreFactorySupplier.get(), messageQueueFactorySupplier.get(), + sessionIDSupplier.get(), dataDictionaryProviderSupplier.get(), sessionScheduleSupplier.get(), logFactorySupplier.get(), messageFactorySupplier.get(), sessionHeartbeatIntervalSupplier.get(), checkLatency, maxLatency, timestampPrecision, resetOnLogon, resetOnLogout, resetOnDisconnect, refreshMessageStoreAtLogon, checkCompID, redundantResentRequestsAllowed, persistMessages, useClosedRangeForResend, @@ -119,7 +125,7 @@ public Session build() { resetOnError, disconnectOnError, disableHeartBeatCheck, false, rejectInvalidMessage, rejectMessageOnUnhandledException, requiresOrigSendingTime, forceResendWhenCorruptedStore, allowedRemoteAddresses, validateIncomingMessage, resendRequestChunkSize, enableNextExpectedMsgSeqNum, - enableLastMsgSeqNumProcessed, validateChecksum); + enableLastMsgSeqNumProcessed, validateChecksum, logonTags, heartBeatTimeoutMultiplier, allowPosDup); } public Builder setBeginString(final String beginString) { @@ -154,6 +160,11 @@ public Builder setMessageStoreFactory(final MessageStoreFactory messageStoreFact return this; } + public Builder setMessageQueueFactory(final MessageQueueFactory messageQueueFactory) { + this.messageQueueFactorySupplier = () -> messageQueueFactory; + return this; + } + public Builder setDataDictionaryProvider(final DataDictionaryProvider dataDictionaryProvider) { this.dataDictionaryProviderSupplier = () -> dataDictionaryProvider; return this; @@ -169,6 +180,11 @@ public Builder setLogFactory(final LogFactory logFactory) { return this; } + public Builder setLogonTags(final List logonTags) { + this.logonTags = logonTags; + return this; + } + public Builder setMessageFactory(final MessageFactory messageFactory) { this.messageFactorySupplier = () -> messageFactory; return this; diff --git a/quickfixj-core/src/test/java/quickfix/SessionResetTest.java b/quickfixj-core/src/test/java/quickfix/SessionResetTest.java index f83cc71720..486a056223 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionResetTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionResetTest.java @@ -55,13 +55,7 @@ public void testSessionResetDeadlock() throws Exception { header.setInt(MsgSeqNum.FIELD, 1); header.setUtcTimeStamp(SendingTime.FIELD, SystemTime.getLocalDateTime(), true); - Thread resetThread = new Thread(() -> { - try { - session.reset(); - } catch (IOException e) { - e.printStackTrace(); - } - }, "SessionReset"); + Thread resetThread = new Thread(session::reset, "SessionReset"); resetThread.setDaemon(true); Thread messageSender = new Thread(() -> { @@ -113,29 +107,38 @@ public String getRemoteAddress() { public void disconnect() { } - public void onConnect() { + public void onConnect(SessionID sessionID) { } - public void onDisconnect() { + public void onDisconnect(SessionID sessionID) { } - public void onLogon() { + public void onLogon(SessionID sessionID) { } - public void onLogout() { + public void onLogout(SessionID sessionID) { } - public void onReset() { + public void onReset(SessionID sessionID) { onResetCalled = true; } - public void onRefresh() { + public void onRefresh(SessionID sessionID) { + } + + public void onMissedHeartBeat(SessionID sessionID) { + } + + public void onHeartBeatTimeout(SessionID sessionID) { + } + + public void onResendRequestSent(SessionID sessionID, int beginSeqNo, int endSeqNo, int currentEndSeqNo) { } - public void onMissedHeartBeat() { + public void onSequenceResetReceived(SessionID sessionID, int newSeqNo, boolean gapFillFlag) { } - public void onHeartBeatTimeout() { + public void onResendRequestSatisfied(SessionID sessionID, int beginSeqNo, int endSeqNo) { } } diff --git a/quickfixj-core/src/test/java/quickfix/SessionSettingsTest.java b/quickfixj-core/src/test/java/quickfix/SessionSettingsTest.java index c3f284bc7f..2de5d1ad10 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionSettingsTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionSettingsTest.java @@ -27,9 +27,11 @@ import java.io.InputStream; import java.security.InvalidParameterException; import java.util.Arrays; +import java.util.ArrayList; import java.util.Iterator; import java.util.Properties; import java.util.Set; +import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.Random; @@ -145,6 +147,14 @@ private SessionSettings setUpSession() throws ConfigError { } public static SessionSettings setUpSession(String extra) throws ConfigError { + String settingsString = getDefaultSettingString(); + if (extra != null) { + settingsString += extra; + } + return createSettingsFromString(settingsString); + } + + private static String getDefaultSettingString() { String settingsString = ""; settingsString += "#comment\n"; settingsString += "[DEFAULT]\n"; @@ -171,10 +181,7 @@ public static SessionSettings setUpSession(String extra) throws ConfigError { settingsString += "BeginString=FIX.4.2\n"; settingsString += "TargetCompID=CLIENT2\n"; settingsString += "DataDictionary=../spec/FIX42.xml\n"; - if (extra != null) { - settingsString += extra; - } - return createSettingsFromString(settingsString); + return settingsString; } private static SessionSettings createSettingsFromString(String settingsString) @@ -183,6 +190,12 @@ private static SessionSettings createSettingsFromString(String settingsString) return new SessionSettings(cfg); } + private static SessionSettings createSettingsFromString(String settingsString, Properties variableValues) + throws ConfigError { + final ByteArrayInputStream cfg = new ByteArrayInputStream(settingsString.getBytes()); + return new SessionSettings(cfg, variableValues); + } + @Test public void testSessionKeyIterator() throws Exception { final SessionSettings settings = setUpSession(); @@ -219,6 +232,17 @@ public void testDefaultsSet() throws Exception { assertEquals("bargle", settings.getString("FileStorePath")); } + @Test + public void testMissingValues() throws ConfigError, FieldConvertError { + final SessionSettings settings = new SessionSettings(); + assertEquals("1", settings.getStringOrDefault("a", "1")); + assertEquals("2", settings.getStringOrDefault("b", "2")); + assertEquals(3, settings.getIntOrDefault("c", 3)); + assertEquals(4, settings.getIntOrDefault("d", 4)); + assertEquals(5L, settings.getLongOrDefault("e", 5L)); + assertEquals(6L, settings.getLongOrDefault("f", 6L)); + } + @Test public void testSpecialCharactersInKeys() throws Exception { final SessionSettings settings = setUpSession("$$$foo bar.baz@@@=value\n"); @@ -313,12 +337,105 @@ public void testVariableInterpolationWithProps() throws Exception { settings.getString("VariableTest")); } + @Test + public void testVariableInterpolationWithCustomPropsForSessionIdFromInputStream() throws Exception { + System.setProperty("test.2", "BAR"); + final Properties properties = new Properties(System.getProperties()); + properties.setProperty("test.1", "FOO"); + + String settingsString = getDefaultSettingString(); + settingsString += "\n"; + settingsString += "[SESSION]\n"; + settingsString += "BeginString=FIX.4.2\n"; + settingsString += "TargetCompID=CLIENT3_${test.1}_${test.2}\n"; + settingsString += "DataDictionary=../spec/FIX42.xml\n"; + + final SessionSettings settings = createSettingsFromString(settingsString, properties); + + SessionID sessionId = findSessionId(settings, "CLIENT3"); + assertNotNull("Settings for CLIENT3 are not found", sessionId); + assertEquals("Wrong TargetCompID", "CLIENT3_FOO_BAR", sessionId.getTargetCompID()); + } + + private SessionID findSessionId(SessionSettings settings, String targetCompIdPrefix) { + Iterator sessionIDIterator = settings.sectionIterator(); + while (sessionIDIterator.hasNext()) { + SessionID sessionID = sessionIDIterator.next(); + if (sessionID.getTargetCompID().startsWith(targetCompIdPrefix)) { + return sessionID; + } + } + return null; + } + + @Test + public void testVariableInterpolationWithCustomPropsForSessionIdFromFile() throws Exception { + System.setProperty("CLIENT_PLACEHOLDER2", "BAR"); + final Properties properties = new Properties(System.getProperties()); + properties.setProperty("CLIENT_PLACEHOLDER1", "FOO"); + + final SessionSettings settings = new SessionSettings(getConfigurationFileName(), properties); + + SessionID sessionId = findSessionId(settings, "CLIENT3"); + assertNotNull("Settings for CLIENT3 are not found", sessionId); + assertEquals("Wrong TargetCompID", "CLIENT3_FOO_BAR", sessionId.getTargetCompID()); +} + @Test public void testDefaultConstructor() { new SessionSettings(); // Passes if no exception is thrown } + @Test + public void testListConstructor() throws ConfigError { + List listValues = new ArrayList(); + listValues.add("[SESSION]"); + listValues.add("BeginString=FIX.4.2"); + listValues.add("SenderCompID=Company"); + listValues.add("SenderSubID=FixedIncome"); + listValues.add("SenderLocationID=HongKong"); + listValues.add("TargetCompID=CLIENT1"); + listValues.add("TargetSubID=HedgeFund"); + listValues.add("TargetLocationID=NYC\n"); + + final SessionSettings settings = new SessionSettings(listValues); + final SessionID id = settings.sectionIterator().next(); + assertEquals("Company", id.getSenderCompID()); + assertEquals("FixedIncome", id.getSenderSubID()); + assertEquals("HongKong", id.getSenderLocationID()); + assertEquals("CLIENT1", id.getTargetCompID()); + assertEquals("HedgeFund", id.getTargetSubID()); + assertEquals("NYC", id.getTargetLocationID()); + } + + @Test + public void testListPropertiesConstructor() throws ConfigError { + System.setProperty("test.2", "BAR"); + final Properties properties = new Properties(System.getProperties()); + properties.setProperty("test.1", "FOO"); + + List listValues = new ArrayList(); + listValues.add("[SESSION]"); + listValues.add("BeginString=FIX.4.2"); + listValues.add("SenderCompID=Company"); + listValues.add("SenderSubID=FixedIncome"); + listValues.add("SenderLocationID=HongKong"); + listValues.add("TargetCompID=CLIENT3_${test.1}_${test.2}"); + listValues.add("TargetSubID=HedgeFund"); + listValues.add("TargetLocationID=NYC\n"); + + final SessionSettings settings = new SessionSettings(listValues, properties); + final SessionID id = settings.sectionIterator().next(); + assertEquals("Company", id.getSenderCompID()); + assertEquals("FixedIncome", id.getSenderSubID()); + assertEquals("HongKong", id.getSenderLocationID()); + assertEquals("CLIENT3_FOO_BAR", id.getTargetCompID()); + assertEquals("HedgeFund", id.getTargetSubID()); + assertEquals("NYC", id.getTargetLocationID()); + + } + @Test public void testConfigError() throws Exception { final InputStream cfg = new InputStream() { @@ -537,4 +654,8 @@ private Map createDefaultSettings() { return defaultSettings; } + private String getConfigurationFileName() { + return "configWithSessionVariables.ini"; + } + } diff --git a/quickfixj-core/src/test/java/quickfix/SessionStateTest.java b/quickfixj-core/src/test/java/quickfix/SessionStateTest.java index a4dbe32158..c058189b2a 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionStateTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionStateTest.java @@ -19,22 +19,32 @@ package quickfix; -import junit.framework.TestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; -public class SessionStateTest extends TestCase { - protected void setUp() throws Exception { - super.setUp(); - MockSystemTimeSource mockTimeSource = new MockSystemTimeSource(1000); - SystemTime.setTimeSource(mockTimeSource); +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SessionStateTest { + + private MockSystemTimeSource timeSource; + + @Before + public void setUp() { + timeSource = new MockSystemTimeSource(1000); + SystemTime.setTimeSource(timeSource); } - protected void tearDown() throws Exception { + @After + public void tearDown() { SystemTime.setTimeSource(null); - super.tearDown(); } - public void testTimeoutDefaultsAreNonzero() throws Exception { - SessionState state = new SessionState(new Object(), null, 0, false, null, Session.DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER); + @Test + public void testTimeoutDefaultsAreNonzero() { + SessionState state = new SessionState(new Object(), null, 0, false, null, + null, Session.DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER); state.setLastReceivedTime(900); assertFalse("logon timeout not init'ed", state.isLogonTimedOut()); @@ -43,8 +53,10 @@ public void testTimeoutDefaultsAreNonzero() throws Exception { assertFalse("logout timeout not init'ed", state.isLogoutTimedOut()); } - public void testTestRequestTiming() throws Exception { - SessionState state = new SessionState(new Object(), null, 0, false, null, Session.DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER); + @Test + public void testTestRequestTiming() { + SessionState state = new SessionState(new Object(), null, 0, false, null, + null, Session.DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER); state.setLastReceivedTime(950); state.setHeartBeatInterval(50); assertFalse("testRequest shouldn't be needed yet", state.isTestRequestNeeded()); @@ -57,4 +69,39 @@ public void testTestRequestTiming() throws Exception { state.setHeartBeatInterval(3); assertFalse("testRequest shouldn't be needed yet", state.isTestRequestNeeded()); } + + @Test + public void testHeartbeatTiming() { + // we set a HB interval of 2 seconds = 2000ms + SessionState state = new SessionState(new Object(), null, 2 /* HB interval */, false, null, + null, Session.DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER); + + long now = System.currentTimeMillis(); + timeSource.setSystemTimes(now); + state.setLastSentTime(now); + assertFalse("heartbeat shouldn't be needed yet", state.isHeartBeatNeeded()); + timeSource.increment(1000); + assertFalse("heartbeat shouldn't be needed yet", state.isHeartBeatNeeded()); + timeSource.increment(1000); + // current time is now 2000ms further since the start, i.e. the HB interval has elapsed + assertTrue("heartbeat should be needed", state.isHeartBeatNeeded()); + } + + @Test + public void testSessionTimeout() { + SessionState state = new SessionState(new Object(), null, 30, false, null, + null, Session.DEFAULT_TEST_REQUEST_DELAY_MULTIPLIER, Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER); + + // session should timeout after 2.4 * 30 = 72 seconds + state.setLastReceivedTime(950_000); + + timeSource.setSystemTimes(1_000_000L); + assertFalse("session is still valid", state.isTimedOut()); + + timeSource.setSystemTimes(1_021_999L); + assertFalse("session is still valid", state.isTimedOut()); + + timeSource.setSystemTimes(1_022_000L); + assertTrue("session timed out", state.isTimedOut()); + } } diff --git a/quickfixj-core/src/test/java/quickfix/SessionTest.java b/quickfixj-core/src/test/java/quickfix/SessionTest.java index 57a8b9ebe5..daadf31032 100644 --- a/quickfixj-core/src/test/java/quickfix/SessionTest.java +++ b/quickfixj-core/src/test/java/quickfix/SessionTest.java @@ -27,6 +27,7 @@ import quickfix.field.Text; import quickfix.field.converter.UtcTimeOnlyConverter; import quickfix.field.converter.UtcTimestampConverter; +import quickfix.field.ResetSeqNumFlag; import quickfix.fix44.Heartbeat; import quickfix.fix44.Logon; import quickfix.fix44.Logout; @@ -46,13 +47,29 @@ import java.lang.reflect.Field; import java.time.Instant; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Date; import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +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 static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; import static quickfix.SessionFactoryTestSupport.createSession; /** @@ -74,19 +91,22 @@ public void testDisposalOfFileResources() throws Exception { final MessageStoreFactory mockMessageStoreFactory = mock(MessageStoreFactory.class); final CloseableMessageStore mockMessageStore = mock(CloseableMessageStore.class); - stub(mockMessageStoreFactory.create(sessionID)).toReturn( - mockMessageStore); + when(mockMessageStoreFactory.create(sessionID)).thenReturn(mockMessageStore); + + final MessageQueueFactory mockMessageQueueFactory = mock(MessageQueueFactory.class); + final MessageQueue mockMessageQueue = mock(MessageQueue.class); + when(mockMessageQueueFactory.create(sessionID)).thenReturn(mockMessageQueue); final LogFactory mockLogFactory = mock(LogFactory.class); final CloseableLog mockLog = mock(CloseableLog.class); - stub(mockLogFactory.create(sessionID)).toReturn(mockLog); + when(mockLogFactory.create(sessionID)).thenReturn(mockLog); try (Session session = new Session(application, - mockMessageStoreFactory, sessionID, null, null, mockLogFactory, + mockMessageStoreFactory, mockMessageQueueFactory, sessionID, null, null, mockLogFactory, new DefaultMessageFactory(), 30, false, 30, UtcTimestampPrecision.MILLIS, true, false, false, false, false, false, true, false, 1.5, null, true, new int[] { 5 }, false, false, false, false, true, false, true, false, - null, true, 0, false, false, true)) { + null, true, 0, false, false, true, new ArrayList<>(), Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false)) { // Simulate socket disconnect session.setResponder(null); } @@ -115,19 +135,22 @@ public void testNondisposableFileResources() throws Exception { final MessageStoreFactory mockMessageStoreFactory = mock(MessageStoreFactory.class); final MessageStore mockMessageStore = mock(MessageStore.class); - stub(mockMessageStoreFactory.create(sessionID)).toReturn( - mockMessageStore); + when(mockMessageStoreFactory.create(sessionID)).thenReturn(mockMessageStore); + + final MessageQueueFactory mockMessageQueueFactory = mock(MessageQueueFactory.class); + final MessageQueue mockMessageQueue = mock(MessageQueue.class); + when(mockMessageQueueFactory.create(sessionID)).thenReturn(mockMessageQueue); final LogFactory mockLogFactory = mock(LogFactory.class); final Log mockLog = mock(Log.class); - stub(mockLogFactory.create(sessionID)).toReturn(mockLog); + when(mockLogFactory.create(sessionID)).thenReturn(mockLog); try (Session session = new Session(application, - mockMessageStoreFactory, sessionID, null, null, mockLogFactory, + mockMessageStoreFactory, mockMessageQueueFactory, sessionID, null, null, mockLogFactory, new DefaultMessageFactory(), 30, false, 30, UtcTimestampPrecision.MILLIS, true, false, false, false, false, false, true, false, 1.5, null, true, new int[] { 5 }, false, false, false, false, true, false, true, false, - null, true, 0, false, false, true)) { + null, true, 0, false, false, true, new ArrayList<>(), Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false)) { // Simulate socket disconnect session.setResponder(null); @@ -541,6 +564,7 @@ public void testLogonLogoutOnAcceptor() throws Exception { // increment time to force logout and reset systemTimeSource.increment(3700000); session.next(); + logoutFrom(session, state.getNextTargetMsgSeqNum()); assertEquals(SystemTime.getDate(), state.getCreationTime()); systemTimeSource.increment(10000); session.next(); @@ -647,6 +671,7 @@ public void testStartOfInitiatorOutsideOfSessionTime() throws Exception { // increase time to be out of session time systemTimeSource.increment(1900000); session.next(); + logoutFrom(session, state.getNextTargetMsgSeqNum()); Message logout = application.lastToAdminMessage(); assertEquals(MsgType.LOGOUT, logout.getHeader() .getString(MsgType.FIELD)); @@ -1024,6 +1049,84 @@ private void setupFileStoreForQFJ357(final SessionID sessionID, } } + @Test + public void testLogonTagsInitiator() throws Exception { + int logonTag1 = 553; // body field + int logonTag2 = 50; // header field + String logonTagValue1 = "foo123=bar"; + String logonTagValue2 = "barsubid"; + SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "foo", "bar"); + String settingsString = ""; + settingsString += "[SESSION]\n"; + settingsString += "BeginString=FIX.4.4\n"; + settingsString += "ConnectionType=initiator\n"; + settingsString += "SocketConnectPort=5001\n"; + settingsString += "SocketConnectHost=localhost\n"; + settingsString += "StartTime=00:00:00\n"; + settingsString += "EndTime=00:00:00\n"; + settingsString += "SenderCompID=foo\n"; + settingsString += "TargetCompID=bar\n"; + settingsString += "HeartBtInt=30\n"; + settingsString += "LogonTag=" + logonTag1 + "=" + logonTagValue1 + "\n"; + settingsString += "LogonTag1=" + logonTag2 + "=" + logonTagValue2 + "\n"; + + SessionSettings settings = SessionSettingsTest.setUpSession(settingsString); + UnitTestApplication application = new UnitTestApplication(); + DefaultSessionFactory sessionFactory = new DefaultSessionFactory(application, new MemoryStoreFactory(), new ScreenLogFactory()); + try (Session session = sessionFactory.create(sessionID, settings)) { + UnitTestResponder responder = new UnitTestResponder(); + session.setResponder(responder); + session.logon(); + session.next(); + Message logonMessage = application.toAdminMessages.get(0); + assertTrue(logonMessage.isSetField(logonTag1)); + assertTrue(logonMessage.getHeader().isSetField(logonTag2)); + assertEquals(logonTagValue1, logonMessage.getString(logonTag1)); + assertEquals(logonTagValue2, logonMessage.getHeader().getString(logonTag2)); + session.getDataDictionary().validate(logonMessage); + } + } + + + @Test + public void testLogonTagsAcceptor() throws Exception { + int logonTag1 = 553; // body field + int logonTag2 = 50; // header field + String logonTagValue1 = "foo123=bar"; + String logonTagValue2 = "barsubid"; + SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "foo", "bar"); + String settingsString = ""; + settingsString += "[SESSION]\n"; + settingsString += "BeginString=FIX.4.4\n"; + settingsString += "ConnectionType=acceptor\n"; + settingsString += "SocketAcceptPort=5001\n"; + settingsString += "StartTime=00:00:00\n"; + settingsString += "EndTime=00:00:00\n"; + settingsString += "SenderCompID=foo\n"; + settingsString += "TargetCompID=bar\n"; + settingsString += "HeartBtInt=30\n"; + settingsString += "DataDictionary=FIX44.xml\n"; + settingsString += "LogonTag=" + logonTag1 + "=" + logonTagValue1 + "\n"; + settingsString += "LogonTag1=" + logonTag2 + "=" + logonTagValue2 + "\n"; + + SessionSettings settings = SessionSettingsTest.setUpSession(settingsString); + UnitTestApplication application = new UnitTestApplication(); + DefaultSessionFactory sessionFactory = new DefaultSessionFactory(application, new MemoryStoreFactory(), new ScreenLogFactory()); + try (Session session = sessionFactory.create(sessionID, settings)) { + UnitTestResponder responder = new UnitTestResponder(); + session.setResponder(responder); + logonTo(session); + session.next(); + assertTrue(session.isLoggedOn()); + Message logonMessage = application.toAdminMessages.get(0); + assertTrue(logonMessage.isSetField(logonTag1)); + assertTrue(logonMessage.getHeader().isSetField(logonTag2)); + assertEquals(logonTagValue1, logonMessage.getString(logonTag1)); + assertEquals(logonTagValue2, logonMessage.getHeader().getString(logonTag2)); + session.getDataDictionary().validate(logonMessage); + } + } + // QFJ-60 @Test public void testRejectLogon() throws Exception { @@ -1118,7 +1221,7 @@ public void testSendingTimeRejectBeforeLogon() throws Exception { session.next(message); - verify(mockStateListener).onDisconnect(); + verify(mockStateListener).onDisconnect(session.getSessionID()); verifyNoMoreInteractions(mockStateListener); } } @@ -1141,7 +1244,7 @@ public void testCorruptLogonReject() throws Exception { session.next(message); - verify(mockStateListener).onDisconnect(); + verify(mockStateListener).onDisconnect(session.getSessionID()); verifyNoMoreInteractions(mockStateListener); } } @@ -1204,7 +1307,8 @@ public void testSequenceResetStackOverflow() throws Exception { try (Session session = setUpSession(application, false, new UnitTestResponder())) { final SessionState state = getSessionState(session); - + final InMemoryMessageQueue queue = (InMemoryMessageQueue) state.getMessageQueue(); + logonTo(session, 1); assertTrue(session.isLoggedOn()); @@ -1227,7 +1331,7 @@ public void testSequenceResetStackOverflow() throws Exception { assertEquals(52, state.getNextTargetMsgSeqNum()); assertTrue(session.isLoggedOn()); assertFalse(state.isResendRequested()); - assertTrue(state.getQueuedSeqNums().isEmpty()); + assertTrue(queue.getBackingMap().isEmpty()); } } @@ -1431,6 +1535,7 @@ public void testRemoveQueuedMessagesOnSequenceReset() throws Exception { try (Session session = setUpSession(application, false, new UnitTestResponder())) { final SessionState state = getSessionState(session); + final InMemoryMessageQueue queue = (InMemoryMessageQueue) state.getMessageQueue(); final int from = 10; int numberOfMsgs = 200; @@ -1442,10 +1547,10 @@ public void testRemoveQueuedMessagesOnSequenceReset() throws Exception { processMessage(session, createAppMessage(i)); } for (int i = from; i < to; i++) { - assertTrue(state.getQueuedSeqNums().contains(i)); + assertTrue(queue.getBackingMap().containsKey(i)); } - assertEquals(state.getQueuedSeqNums().size(), numberOfMsgs); + assertEquals(queue.getBackingMap().size(), numberOfMsgs); assertTrue(application.fromAppMessages.isEmpty()); // Create a sequence reset which will cause deletion of almost all // messages @@ -1457,7 +1562,7 @@ public void testRemoveQueuedMessagesOnSequenceReset() throws Exception { assertEquals(application.fromAppMessages.size(), two); assertFalse(state.isResendRequested()); assertTrue(session.isLoggedOn()); - assertTrue(state.getQueuedSeqNums().isEmpty()); + assertTrue(queue.getBackingMap().isEmpty()); } } @@ -1779,7 +1884,7 @@ public void testNonpersistedGapFill() throws Exception { @Test // QFJ-457 - public void testAcceptorRelogon() throws Exception { + public void testAcceptorRejectsLogonWhenLogoutInitiatedLocally() throws Exception { final UnitTestApplication application = new UnitTestApplication(); try (Session session = setUpSession(application, false, new UnitTestResponder())) { @@ -1798,13 +1903,40 @@ public void testAcceptorRelogon() throws Exception { UtcTimestampConverter.convert(LocalDateTime.now(ZoneOffset.UTC), UtcTimestampPrecision.SECONDS)); logout.getHeader().setInt(MsgSeqNum.FIELD, 2); session.next(logout); - - // session.reset(); + + assertFalse(session.isEnabled()); assertFalse(session.isLoggedOn()); logonTo(session, 3); Message lastToAdminMessage = application.lastToAdminMessage(); - assertNotEquals(Logout.MSGTYPE, lastToAdminMessage.getHeader() - .getString(MsgType.FIELD)); + assertEquals(Logout.MSGTYPE, lastToAdminMessage.getHeader().getString(MsgType.FIELD)); + } + } + + @Test + public void testAcceptorAcceptsLogonWhenLogoutInitiatedExternally() throws Exception { + final UnitTestApplication application = new UnitTestApplication(); + try (Session session = setUpSession(application, false, + new UnitTestResponder())) { + + logonTo(session); + assertTrue(session.isEnabled()); + assertTrue(session.isLoggedOn()); + + final Message logout = new Logout(); + logout.getHeader().setString(SenderCompID.FIELD, "TARGET"); + logout.getHeader().setString(TargetCompID.FIELD, "SENDER"); + logout.getHeader().setString(SendingTime.FIELD, + UtcTimestampConverter.convert(LocalDateTime.now(ZoneOffset.UTC), UtcTimestampPrecision.SECONDS)); + logout.getHeader().setInt(MsgSeqNum.FIELD, 2); + session.next(logout); + + session.next(); + + assertTrue(session.isEnabled()); + assertFalse(session.isLoggedOn()); + logonTo(session, 3); + Message lastToAdminMessage = application.lastToAdminMessage(); + assertEquals(Logon.MSGTYPE, lastToAdminMessage.getHeader().getString(MsgType.FIELD)); } } @@ -1889,7 +2021,7 @@ public void testGenerateRejectAndTargetSeqNum() throws Exception { "8=FIX.4.2\0019=0113\00135=4\00134=223\00143=Y\001122=20100908-17:59:30.642\00149=THEM\00156=US\001369=178\00152=20100908-17:59:30.642\001123=Y\00136=225\00110=110\001", "8=FIX.4.2\0019=0246\00135=8\001115=THEM\00134=225\00143=Y\001122=20100908-17:52:37.920\00149=THEM\00156=US\001369=178\00152=20100908-17:59:30.642\00137=10118506\00111=a00000052.1\00117=17537743\00120=0\001150=4\00139=4\00155=ETFC\00154=1\00138=500000\00144=0.998\00132=0\00131=0\001151=0\00114=0\0016=0\00160=20100908-17:52:37.920\00110=80\001" }; for (String message : messages) - session.next(MessageUtils.parse(session, message)); + session.next(MessageSessionUtils.parse(session, message)); assertEquals(226, session.getStore().getNextTargetMsgSeqNum()); } @@ -1975,13 +2107,13 @@ private void testSequenceResetGapFillWithChunkSize(int chunkSize) boolean isInitiator = true, resetOnLogon = false, validateSequenceNumbers = true; try (Session session = new Session(new UnitTestApplication(), - new MemoryStoreFactory(), sessionID, null, null, + new MemoryStoreFactory(), new InMemoryMessageQueueFactory(), sessionID, null, null, new SLF4JLogFactory(new SessionSettings()), new DefaultMessageFactory(), isInitiator ? 30 : 0, false, 30, UtcTimestampPrecision.MILLIS, resetOnLogon, false, false, false, false, false, true, false, 1.5, null, validateSequenceNumbers, new int[] { 5 }, false, false, false, false, true, false, true, false, null, true, - chunkSize, false, false, true)) { + chunkSize, false, false, true, new ArrayList<>(), Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false)) { UnitTestResponder responder = new UnitTestResponder(); session.setResponder(responder); @@ -2038,12 +2170,12 @@ public void correct_sequence_number_for_last_gap_fill_if_next_sender_sequence_nu final boolean resetOnLogon = false; final boolean validateSequenceNumbers = true; - Session session = new Session(new UnitTestApplication(), new MemoryStoreFactory(), + Session session = new Session(new UnitTestApplication(), new MemoryStoreFactory(), new InMemoryMessageQueueFactory(), sessionID, null, null, null, new DefaultMessageFactory(), 30, false, 30, UtcTimestampPrecision.MILLIS, resetOnLogon, false, false, false, false, false, true, false, 1.5, null, validateSequenceNumbers, new int[]{5}, false, false, false, false, true, false, true, false, null, true, 0, - false, false, true); + false, false, true, new ArrayList<>(), Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false); Responder mockResponder = mock(Responder.class); when(mockResponder.send(anyString())).thenReturn(true); @@ -2086,12 +2218,12 @@ public void correct_sequence_number_for_last_gap_fill_if_next_sender_sequence_nu final boolean resetOnLogon = false; final boolean validateSequenceNumbers = true; - Session session = new Session(new UnitTestApplication(), new MemoryStoreFactory(), + Session session = new Session(new UnitTestApplication(), new MemoryStoreFactory(), new InMemoryMessageQueueFactory(), sessionID, null, null, null, new DefaultMessageFactory(), 30, false, 30, UtcTimestampPrecision.MILLIS, resetOnLogon, false, false, false, false, false, true, false, 1.5, null, validateSequenceNumbers, new int[]{5}, false, false, false, false, true, false, true, false, null, true, 0, - enableNextExpectedMsgSeqNum, false, true); + enableNextExpectedMsgSeqNum, false, true, new ArrayList<>(), Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false); Responder mockResponder = mock(Responder.class); when(mockResponder.send(anyString())).thenReturn(true); @@ -2134,13 +2266,13 @@ public void testMsgSeqNumTooHighWithDisconnectOnError() throws Exception { final boolean disconnectOnError = true; try (Session session = new Session(new UnitTestApplication(), - new MemoryStoreFactory(), sessionID, null, null, + new MemoryStoreFactory(), new InMemoryMessageQueueFactory(), sessionID, null, null, new SLF4JLogFactory(new SessionSettings()), new DefaultMessageFactory(), isInitiator ? 30 : 0, false, 30, UtcTimestampPrecision.MILLIS, resetOnLogon, false, false, false, false, false, true, false, 1.5, null, validateSequenceNumbers, new int[] { 5 }, false, disconnectOnError, false, false, true, false, true, false, - null, true, 0, false, false, true)) { + null, true, 0, false, false, true, new ArrayList<>(), Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false)) { UnitTestResponder responder = new UnitTestResponder(); session.setResponder(responder); @@ -2170,13 +2302,13 @@ public void testTimestampPrecision() throws Exception { UnitTestApplication unitTestApplication = new UnitTestApplication(); try (Session session = new Session(unitTestApplication, - new MemoryStoreFactory(), sessionID, null, null, + new MemoryStoreFactory(), new InMemoryMessageQueueFactory(), sessionID, null, null, new SLF4JLogFactory(new SessionSettings()), new DefaultMessageFactory(), isInitiator ? 30 : 0, false, 30, UtcTimestampPrecision.NANOS, resetOnLogon, false, false, false, false, false, true, false, 1.5, null, validateSequenceNumbers, new int[] { 5 }, false, disconnectOnError, false, false, true, false, true, false, - null, true, 0, false, false, true)) { + null, true, 0, false, false, true, new ArrayList<>(), Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false)) { UnitTestResponder responder = new UnitTestResponder(); session.setResponder(responder); @@ -2223,12 +2355,12 @@ private void testLargeQueue(int N) throws Exception { boolean isInitiator = true, resetOnLogon = false, validateSequenceNumbers = true; final UnitTestApplication unitTestApplication = new UnitTestApplication(); - Session session = new Session(unitTestApplication, new MemoryStoreFactory(), + Session session = new Session(unitTestApplication, new MemoryStoreFactory(), new InMemoryMessageQueueFactory(), sessionID, null, null, null, new DefaultMessageFactory(), isInitiator ? 30 : 0, false, 30, UtcTimestampPrecision.MILLIS, resetOnLogon, false, false, false, false, false, true, false, 1.5, null, validateSequenceNumbers, new int[]{5}, false, false, false, false, true, false, true, false, null, true, 0, - false, false, true); + false, false, true, new ArrayList<>(), Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false); UnitTestResponder responder = new UnitTestResponder(); session.setResponder(responder); @@ -2339,12 +2471,12 @@ public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound } }; - Session session = new Session(app, new MemoryStoreFactory(), + Session session = new Session(app, new MemoryStoreFactory(), new InMemoryMessageQueueFactory(), sessionID, null, null, null, new DefaultMessageFactory(), isInitiator ? 30 : 0, false, 30, UtcTimestampPrecision.MILLIS, resetOnLogon, false, false, false, false, false, true, false, 1.5, null, validateSequenceNumbers, new int[]{5}, false, false, false, false, true, false, true, false, null, true, 0, - enableNextExpectedMsgSeqNum, false, true); + enableNextExpectedMsgSeqNum, false, true, new ArrayList<>(), Session.DEFAULT_HEARTBEAT_TIMEOUT_MULTIPLIER, false); UnitTestResponder responder = new UnitTestResponder(); session.setResponder(responder); @@ -2377,6 +2509,426 @@ public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound assertEquals(0, app.lastToAdminMessage().getInt(EndSeqNo.FIELD)); } + /** + * https://github.com/quickfix-j/quickfixj/issues/244 + * */ + @Test + public void acceptorSession_ShouldWaitForLogoutResponseBeforeDisconnecting_WhenSendingLogoutDuringResetInSessionTime() + throws ConfigError, IOException, IllegalAccessException, FieldConvertError, NoSuchFieldException, + InvalidMessage, FieldNotFound, RejectLogon, UnsupportedMessageType, IncorrectTagValue, IncorrectDataFormat { + + final UnitTestApplication application = new UnitTestApplication(); + final UnitTestResponder responder = new UnitTestResponder(); + final MockSystemTimeSource systemTimeSource = new MockSystemTimeSource(); + final int startTimeEndTimeOffsetHours = 1; + + try (Session session = + setupFor244(application, responder, systemTimeSource, startTimeEndTimeOffsetHours, false)) { + + final SessionState state = getSessionState(session); + + logonTo(session); + + session.reset(); + + logoutFrom(session, state.getNextTargetMsgSeqNum()); + } + + assertFor244(application, responder, 1, 1, 1, true); + } + + /** + * https://github.com/quickfix-j/quickfixj/issues/244 + * */ + @Test + public void acceptorSession_ShouldWaitForLogoutResponseBeforeDisconnecting_WhenSendingLogoutDuringResetOutsideSessionTime() + throws ConfigError, IOException, IllegalAccessException, FieldConvertError, NoSuchFieldException, + InvalidMessage, FieldNotFound, RejectLogon, UnsupportedMessageType, IncorrectTagValue, IncorrectDataFormat { + + final UnitTestApplication application = new UnitTestApplication(); + final UnitTestResponder responder = new UnitTestResponder(); + final MockSystemTimeSource systemTimeSource = new MockSystemTimeSource(); + final int startTimeEndTimeOffsetHours = 1; + + try (Session session = + setupFor244(application, responder, systemTimeSource, startTimeEndTimeOffsetHours, false)) { + + final SessionState state = getSessionState(session); + + logonTo(session); + + systemTimeSource.increment(TimeUnit.HOURS.toMillis(startTimeEndTimeOffsetHours) * 2); + + session.reset(); + + logoutFrom(session, state.getNextTargetMsgSeqNum()); + } + + assertFor244(application, responder, 1, 1, 1, true); + + } + + /** + * https://github.com/quickfix-j/quickfixj/issues/244 + * */ + @Test + public void initiatorSession_ShouldWaitForLogoutResponseBeforeDisconnecting_WhenSendingLogoutDuringResetInSessionTime() + throws ConfigError, IOException, IllegalAccessException, FieldConvertError, NoSuchFieldException, + InvalidMessage, FieldNotFound, RejectLogon, UnsupportedMessageType, IncorrectTagValue, IncorrectDataFormat { + + final UnitTestApplication application = new UnitTestApplication(); + final UnitTestResponder responder = new UnitTestResponder(); + final MockSystemTimeSource systemTimeSource = new MockSystemTimeSource(); + final int startTimeEndTimeOffsetHours = 1; + + try (Session session = + setupFor244(application, responder, systemTimeSource, startTimeEndTimeOffsetHours, true)) { + + final SessionState state = getSessionState(session); + + session.next(); + + logonTo(session); + + session.reset(); + + logoutFrom(session, state.getNextTargetMsgSeqNum()); + } + + assertFor244(application, responder, 1, 1, 1, true); + } + + /** + * https://github.com/quickfix-j/quickfixj/issues/244 + * */ + @Test + public void initiatorSession_ShouldWaitForLogoutResponseBeforeDisconnecting_WhenSendingLogoutDuringResetOutsideSessionTime() + throws ConfigError, IOException, IllegalAccessException, FieldConvertError, NoSuchFieldException, + InvalidMessage, FieldNotFound, RejectLogon, UnsupportedMessageType, IncorrectTagValue, IncorrectDataFormat { + + final UnitTestApplication application = new UnitTestApplication(); + final UnitTestResponder responder = new UnitTestResponder(); + final MockSystemTimeSource systemTimeSource = new MockSystemTimeSource(); + final int startTimeEndTimeOffsetHours = 1; + + try (Session session = + setupFor244(application, responder, systemTimeSource, startTimeEndTimeOffsetHours, true)) { + + final SessionState state = getSessionState(session); + + session.next(); + + logonTo(session); + + systemTimeSource.increment(TimeUnit.HOURS.toMillis(startTimeEndTimeOffsetHours) * 2); + + session.reset(); + + logoutFrom(session, state.getNextTargetMsgSeqNum()); + } + + assertFor244(application, responder, 1, 1, 1, true); + } + + /** + * https://github.com/quickfix-j/quickfixj/issues/244 + * */ + @Test + public void acceptorSession_ShouldWaitForLogoutTimeoutBeforeDisconnecting_WhenSendingLogoutDuringResetInSessionTime() + throws ConfigError, IOException, IllegalAccessException, FieldConvertError, NoSuchFieldException, + InvalidMessage, FieldNotFound, RejectLogon, UnsupportedMessageType, IncorrectTagValue, IncorrectDataFormat { + + final UnitTestApplication application = new UnitTestApplication(); + final UnitTestResponder responder = new UnitTestResponder(); + final MockSystemTimeSource systemTimeSource = new MockSystemTimeSource(); + final int startTimeEndTimeOffsetHours = 1; + + try (Session session = + setupFor244(application, responder, systemTimeSource, startTimeEndTimeOffsetHours, false)) { + + final SessionState state = getSessionState(session); + final long logoutTimeoutMs = TimeUnit.SECONDS.toMillis(state.getLogoutTimeout()); + + logonTo(session); + + session.reset(); + + systemTimeSource.increment(logoutTimeoutMs * 2); + + session.next(); + } + + assertFor244(application, responder, 1, 0, 1, true); + } + + /** + * https://github.com/quickfix-j/quickfixj/issues/244 + * */ + @Test + public void acceptorSession_ShouldWaitForLogoutTimeoutBeforeDisconnecting_WhenSendingLogoutDuringResetOutsideSessionTime() + throws ConfigError, IOException, IllegalAccessException, FieldConvertError, NoSuchFieldException, + InvalidMessage, FieldNotFound, RejectLogon, UnsupportedMessageType, IncorrectTagValue, IncorrectDataFormat { + + final UnitTestApplication application = new UnitTestApplication(); + final UnitTestResponder responder = new UnitTestResponder(); + final MockSystemTimeSource systemTimeSource = new MockSystemTimeSource(); + final int startTimeEndTimeOffsetHours = 1; + + try (Session session = + setupFor244(application, responder, systemTimeSource, startTimeEndTimeOffsetHours, false)) { + + final SessionState state = getSessionState(session); + final long logoutTimeoutMs = TimeUnit.SECONDS.toMillis(state.getLogoutTimeout()); + + logonTo(session); + + systemTimeSource.increment(TimeUnit.HOURS.toMillis(startTimeEndTimeOffsetHours) * 2); + + session.reset(); + + systemTimeSource.increment(logoutTimeoutMs * 2); + + session.next(); + } + + assertFor244(application, responder, 1, 0, 1, true); + } + + /** + * https://github.com/quickfix-j/quickfixj/issues/244 + * */ + @Test + public void initiatorSession_ShouldWaitForLogoutTimeoutBeforeDisconnecting_WhenSendingLogoutDuringResetInSessionTime() + throws ConfigError, IOException, IllegalAccessException, FieldConvertError, NoSuchFieldException, + InvalidMessage, FieldNotFound, RejectLogon, UnsupportedMessageType, IncorrectTagValue, IncorrectDataFormat { + + final UnitTestApplication application = new UnitTestApplication(); + final UnitTestResponder responder = new UnitTestResponder(); + final MockSystemTimeSource systemTimeSource = new MockSystemTimeSource(); + final int startTimeEndTimeOffsetHours = 1; + + try (Session session = + setupFor244(application, responder, systemTimeSource, startTimeEndTimeOffsetHours, true)) { + + final SessionState state = getSessionState(session); + final long logoutTimeoutMs = TimeUnit.SECONDS.toMillis(state.getLogoutTimeout()); + + session.next(); + + logonTo(session); + + session.reset(); + + systemTimeSource.increment(logoutTimeoutMs * 2); + + session.next(); + } + + assertFor244(application, responder, 1, 0, 1, true); + } + + /** + * https://github.com/quickfix-j/quickfixj/issues/244 + * */ + @Test + public void initiatorSession_ShouldWaitForLogoutTimeoutBeforeDisconnecting_WhenSendingLogoutDuringResetOutsideSessionTime() + throws ConfigError, IOException, IllegalAccessException, FieldConvertError, NoSuchFieldException, + InvalidMessage, FieldNotFound, RejectLogon, UnsupportedMessageType, IncorrectTagValue, IncorrectDataFormat { + + final UnitTestApplication application = new UnitTestApplication(); + final UnitTestResponder responder = new UnitTestResponder(); + final MockSystemTimeSource systemTimeSource = new MockSystemTimeSource(); + final int startTimeEndTimeOffsetHours = 1; + + try (Session session = + setupFor244(application, responder, systemTimeSource, startTimeEndTimeOffsetHours, true)) { + + final SessionState state = getSessionState(session); + final long logoutTimeoutMs = TimeUnit.SECONDS.toMillis(state.getLogoutTimeout()); + + session.next(); + + logonTo(session); + + systemTimeSource.increment(TimeUnit.HOURS.toMillis(startTimeEndTimeOffsetHours) * 2); + + session.reset(); + + systemTimeSource.increment(logoutTimeoutMs * 2); + + session.next(); + } + + assertFor244(application, responder, 1, 0, 1, true); + } + + /** + * https://github.com/quickfix-j/quickfixj/issues/474 + * */ + @Test + public void msgseqnum_getting_reset_in_rejected_logon_scenario_fix() throws Exception { + + final UnitTestApplication application = new UnitTestApplication(){ + int logonCount = 0; + @Override + public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, + IncorrectDataFormat, IncorrectTagValue, RejectLogon { + super.fromAdmin(message, sessionId); + if (message.getHeader().getString(MsgType.FIELD).equals(Logon.MSGTYPE)) { + logonCount += 1; + } + if (logonCount == 2) { + logonCount += 1; + throw new RejectLogon("RejectLogon"); + } + } + }; + + final SessionID sessionID = new SessionID( + FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + + Session initSession = createSession(sessionID, application, true, + true, true); + final SessionState state = getSessionState(initSession); + + final UnitTestResponder responder = new UnitTestResponder(); + + try{ + initSession.setResponder(responder); + initSession.next(); + logonTo(initSession); + + Message logon = application.toAdminMessages.get(0); + assertEquals(MsgType.LOGON, logon.getHeader().getString(MsgType.FIELD)); + + + assertEquals(2, state.getNextSenderMsgSeqNum()); + assertTrue(initSession.isLoggedOn()); + + initSession.generateHeartbeat(); + initSession.next(createHeartbeatMessage(2)); + assertEquals(3, state.getNextSenderMsgSeqNum()); + + initSession.generateHeartbeat(); + initSession.next(createHeartbeatMessage(3)); + assertEquals(4, state.getNextSenderMsgSeqNum()); + + initSession.logout(); + initSession.next(); + + assertEquals(5, state.getNextSenderMsgSeqNum()); + assertEquals(4, state.getNextTargetMsgSeqNum()); + + + } catch (Exception e) { + throw new Exception(e);} + + try { + initSession.logon(); + + // logon message which will get rejected + initSession.next(); + logonTo(initSession,1,true); + + } catch (Exception e){ + throw e; + } + + assertEquals(6, state.getNextSenderMsgSeqNum()); + assertEquals(4, state.getNextTargetMsgSeqNum()); + + } + + /** + * https://github.com/quickfix-j/quickfixj/issues/244 + * */ + @SuppressWarnings("SameParameterValue") + private Session setupFor244(UnitTestApplication application, + UnitTestResponder responder, + MockSystemTimeSource systemTimeSource, + int startTimeEndTimeOffsetHours, + boolean isInitiator) + throws ConfigError, FieldConvertError, IOException, NoSuchFieldException, IllegalAccessException { + + final LocalDateTime now = LocalDateTime.now(); + final ZoneOffset zoneOffset = ZoneOffset.systemDefault() + .getRules() + .getOffset(now); + final Instant nowInstant = now.toInstant(zoneOffset); + final long nowEpocMillis = nowInstant.toEpochMilli(); + + systemTimeSource.setSystemTimes(nowEpocMillis); + SystemTime.setTimeSource(systemTimeSource); + + final LocalTime nowLocalTime = now.toLocalTime(); + final LocalTime nowLocalTimeMinusStartTimeEndTimeOffsetHours = + nowLocalTime.minusHours(startTimeEndTimeOffsetHours); + final LocalTime nowLocalTimePlusStartTimeEndTimeOffsetHours = + nowLocalTime.plusHours(startTimeEndTimeOffsetHours); + final String startTime = + UtcTimeOnlyConverter.convert(nowLocalTimeMinusStartTimeEndTimeOffsetHours, UtcTimestampPrecision.SECONDS); + final String endTime = + UtcTimeOnlyConverter.convert(nowLocalTimePlusStartTimeEndTimeOffsetHours, UtcTimestampPrecision.SECONDS); + final String timeZone = TimeZone.getDefault() + .getID(); + + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + + final SessionSettings sessionSettings = SessionSettingsTest.setUpSession(null); + sessionSettings.setString(Session.SETTING_START_TIME, startTime); + sessionSettings.setString(Session.SETTING_END_TIME, endTime); + sessionSettings.setString(Session.SETTING_TIMEZONE, timeZone); + + setupFileStoreForQFJ357(sessionID, sessionSettings); + + Session session = setUpFileStoreSession(application, isInitiator, responder, sessionSettings, sessionID); + session.addStateListener(application); + + return session; + } + + /** + * https://github.com/quickfix-j/quickfixj/issues/244 + * */ + @SuppressWarnings("SameParameterValue") + private void assertFor244(UnitTestApplication application, + UnitTestResponder responder, + long expectedLogoutMessageSentCount, + long expectedLogoutMessageReceivedCount, + int expectedSessionResetCount, + boolean expectedDisconnectCalled) { + + final long actualLogoutMessageSentCount = application.toAdminMessages.stream() + .filter(message -> message instanceof Logout) + .count(); + final long actualLogoutMessageReceivedCount = application.fromAdminMessages.stream() + .filter(message -> message instanceof Logout) + .count(); + final int actualSessionResets = application.sessionResets; + final boolean actualDisconnectCalled = responder.disconnectCalled; + + assertEquals( + String.format("Expected logout message sent count: %d.", expectedLogoutMessageSentCount), + expectedLogoutMessageSentCount, + actualLogoutMessageSentCount + ); + assertEquals( + String.format("Expected logout message received count: %d.", expectedLogoutMessageReceivedCount), + expectedLogoutMessageReceivedCount, + actualLogoutMessageReceivedCount + ); + assertEquals( + String.format("Expected session reset count: %d.", expectedSessionResetCount), + expectedSessionResetCount, + actualSessionResets + ); + assertEquals( + String.format("Expected disconnect called: %b.", expectedDisconnectCalled), + expectedDisconnectCalled, + actualDisconnectCalled + ); + } + private News createPossDupAppMessage(int sequence) { // create a regular app message and and add the PossDup // and OrigSendingTime tags to it @@ -2466,6 +3018,20 @@ private void logonTo(Session session, int sequence) throws FieldNotFound, final Logon receivedLogon = new Logon(); setUpHeader(session.getSessionID(), receivedLogon, true, sequence); receivedLogon.setInt(HeartBtInt.FIELD, 30); + receivedLogon.setInt(EncryptMethod.FIELD, 0); + receivedLogon.toString(); // calculate length and checksum + session.next(receivedLogon); + } + + private void logonTo(Session session, int sequence, boolean resetSeqNumFlag) throws FieldNotFound, + RejectLogon, IncorrectDataFormat, IncorrectTagValue, + UnsupportedMessageType, IOException, InvalidMessage { + final Logon receivedLogon = new Logon(); + setUpHeader(session.getSessionID(), receivedLogon, true, sequence); + receivedLogon.setInt(HeartBtInt.FIELD, 30); + receivedLogon.setInt(EncryptMethod.FIELD, 0); + receivedLogon.setBoolean(ResetSeqNumFlag.FIELD, resetSeqNumFlag); + receivedLogon.toString(); // calculate length and checksum session.next(receivedLogon); } @@ -2513,4 +3079,70 @@ public void disconnect() { } } + @Test + public void testSendWithAllowPosDupAsFalse_ShouldRemovePossDupFlagAndOrigSendingTime() throws Exception { + final UnitTestApplication application = new UnitTestApplication(); + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + final Session session = SessionFactoryTestSupport.createSession(sessionID, application, false, false, true, true, null); + UnitTestResponder responder = new UnitTestResponder(); + session.setResponder(responder); + logonTo(session); + + session.send(createPossDupAppMessage(1), false); + + final Message sentMessage = new Message(responder.sentMessageData); + + assertFalse(sentMessage.getHeader().isSetField(PossDupFlag.FIELD)); + assertFalse(sentMessage.getHeader().isSetField(OrigSendingTime.FIELD)); + } + + @Test + public void testSendWithAllowPosDupAsFalse_ShouldRemovePossDupFlagAndOrigSendingTime_GivenAllowPosDupConfigurationPropertySetToTrue() throws Exception { + final UnitTestApplication application = new UnitTestApplication(); + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + final Session session = SessionFactoryTestSupport.createSession(sessionID, application, false, false, true, true, null); + UnitTestResponder responder = new UnitTestResponder(); + session.setResponder(responder); + session.setAllowPosDup(true); + logonTo(session); + session.send(createPossDupAppMessage(1), false); + + final Message sentMessage = new Message(responder.sentMessageData); + + assertFalse(sentMessage.getHeader().isSetField(PossDupFlag.FIELD)); + assertFalse(sentMessage.getHeader().isSetField(OrigSendingTime.FIELD)); + } + + @Test + public void testSendWithAllowPosDupAsTrue_ShouldKeepPossDupFlagAndOrigSendingTime() throws Exception { + final UnitTestApplication application = new UnitTestApplication(); + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + final Session session = SessionFactoryTestSupport.createSession(sessionID, application, false, false, true, true, null); + UnitTestResponder responder = new UnitTestResponder(); + session.setResponder(responder); + logonTo(session); + session.send(createPossDupAppMessage(1), true); + + final Message sentMessage = new Message(responder.sentMessageData); + + assertTrue(sentMessage.getHeader().isSetField(PossDupFlag.FIELD)); + assertTrue(sentMessage.getHeader().isSetField(OrigSendingTime.FIELD)); + } + + @Test + public void testSend_ShouldKeepPossDupFlagAndOrigSendingTime_GivenAllowPosDupConfigurationPropertySetToTrue() throws Exception { + final UnitTestApplication application = new UnitTestApplication(); + final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "SENDER", "TARGET"); + final Session session = SessionFactoryTestSupport.createSession(sessionID, application, false, false, true, true, null); + UnitTestResponder responder = new UnitTestResponder(); + session.setResponder(responder); + session.setAllowPosDup(true); + logonTo(session); + session.send(createPossDupAppMessage(1)); + + final Message sentMessage = new Message(responder.sentMessageData); + + assertTrue(sentMessage.getHeader().isSetField(PossDupFlag.FIELD)); + assertTrue(sentMessage.getHeader().isSetField(OrigSendingTime.FIELD)); + } } diff --git a/quickfixj-core/src/test/java/quickfix/SleepycatStoreTest.java b/quickfixj-core/src/test/java/quickfix/SleepycatStoreTest.java index ec84e6a9b3..fd3bf23808 100644 --- a/quickfixj-core/src/test/java/quickfix/SleepycatStoreTest.java +++ b/quickfixj-core/src/test/java/quickfix/SleepycatStoreTest.java @@ -23,7 +23,8 @@ import java.io.IOException; public class SleepycatStoreTest extends AbstractMessageStoreTest { - protected MessageStoreFactory getMessageStoreFactory() throws ConfigError, FieldConvertError { + + protected MessageStoreFactory getMessageStoreFactory() throws ConfigError { SessionSettings settings = new SessionSettings(getConfigurationFileName()); File tmpfile; try { @@ -62,5 +63,4 @@ public void testCloseAndOpen() throws Exception { assertEquals(123, store.getNextSenderMsgSeqNum()); assertEquals(321, store.getNextTargetMsgSeqNum()); } - } diff --git a/quickfixj-core/src/test/java/quickfix/SocketAcceptorTest.java b/quickfixj-core/src/test/java/quickfix/SocketAcceptorTest.java index 0d0ff175e8..480416b978 100644 --- a/quickfixj-core/src/test/java/quickfix/SocketAcceptorTest.java +++ b/quickfixj-core/src/test/java/quickfix/SocketAcceptorTest.java @@ -19,6 +19,8 @@ package quickfix; +import org.apache.mina.core.service.IoAcceptor; +import org.apache.mina.util.AvailablePortFinder; import org.junit.After; import org.junit.Test; import org.slf4j.Logger; @@ -26,7 +28,10 @@ import quickfix.field.MsgType; import quickfix.mina.ProtocolFactory; import quickfix.mina.SingleThreadedEventHandlingStrategy; +import quickfix.mina.message.FIXProtocolCodecFactory; +import quickfix.mina.ssl.SSLSupport; +import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; @@ -35,7 +40,13 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; -import static org.junit.Assert.*; +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.test.util.StackTraceUtil; /** * QFJ-643: Unable to restart a stopped acceptor (SocketAcceptor) @@ -65,15 +76,16 @@ public void cleanup() { @Test public void testRestartOfAcceptor() throws Exception { - TestAcceptorApplication testAcceptorApplication = new TestAcceptorApplication(); - TestInitiatorApplication testInitiatorApplication = new TestInitiatorApplication(); + TestConnectorApplication testAcceptorApplication = new TestConnectorApplication(); + TestConnectorApplication testInitiatorApplication = new TestConnectorApplication(); ThreadMXBean bean = ManagementFactory.getThreadMXBean(); Acceptor acceptor = null; Initiator initiator = null; try { - acceptor = createAcceptor(testAcceptorApplication); + final int port = AvailablePortFinder.getNextAvailable(); + acceptor = createAcceptor(testAcceptorApplication, port); acceptor.start(); - initiator = createInitiator(testInitiatorApplication); + initiator = createInitiator(testInitiatorApplication, port); assertNotNull("Session should be registered", lookupSession(acceptorSessionID)); @@ -92,22 +104,25 @@ public void testRestartOfAcceptor() throws Exception { assertTrue("acceptor should have logged on by now", acceptor.isLoggedOn()); assertTrue("initiator should have logged on by now", initiator.isLoggedOn()); } finally { - if (initiator != null) { - try { - initiator.stop(); - } catch (RuntimeException e) { - log.error(e.getMessage(), e); + try { + if (initiator != null) { + try { + initiator.stop(); + } catch (RuntimeException e) { + log.error(e.getMessage(), e); + } } - } - if (acceptor != null) { - try { - acceptor.stop(); - } catch (RuntimeException e) { - log.error(e.getMessage(), e); + testAcceptorApplication.waitForLogout(); + } finally { + if (acceptor != null) { + try { + acceptor.stop(); + } catch (RuntimeException e) { + log.error(e.getMessage(), e); + } } + testInitiatorApplication.waitForLogout(); } - testAcceptorApplication.waitForLogout(); - testInitiatorApplication.waitForLogout(); } } @@ -117,8 +132,9 @@ public void testQuickRestartOfAcceptor() throws Exception { Acceptor acceptor = null; try { ThreadMXBean bean = ManagementFactory.getThreadMXBean(); - TestAcceptorApplication testAcceptorApplication = new TestAcceptorApplication(); - acceptor = createAcceptor(testAcceptorApplication); + TestConnectorApplication testAcceptorApplication = new TestConnectorApplication(); + final int port = AvailablePortFinder.getNextAvailable(); + acceptor = createAcceptor(testAcceptorApplication, port); acceptor.start(); Thread.sleep(2500L); acceptor.stop(); @@ -138,8 +154,9 @@ public void testDoubleStartOfAcceptor() throws Exception { Acceptor acceptor = null; try { ThreadMXBean bean = ManagementFactory.getThreadMXBean(); - TestAcceptorApplication testAcceptorApplication = new TestAcceptorApplication(); - acceptor = createAcceptor(testAcceptorApplication); + TestConnectorApplication testAcceptorApplication = new TestConnectorApplication(); + final int port = AvailablePortFinder.getNextAvailable(); + acceptor = createAcceptor(testAcceptorApplication, port); acceptor.start(); // second start should be ignored acceptor.start(); @@ -156,8 +173,9 @@ public void testDoubleStartOfAcceptor() throws Exception { public void testSessionsAreCleanedUp() throws Exception { Acceptor acceptor = null; try { - TestAcceptorApplication testAcceptorApplication = new TestAcceptorApplication(); - acceptor = createAcceptor(testAcceptorApplication); + TestConnectorApplication testAcceptorApplication = new TestConnectorApplication(); + final int port = AvailablePortFinder.getNextAvailable(); + acceptor = createAcceptor(testAcceptorApplication, port); acceptor.start(); assertEquals(1, acceptor.getSessions().size() ); assertEquals(1 + SESSION_COUNT, Session.numSessions() ); @@ -175,8 +193,9 @@ public void testSessionsAreCleanedUp() throws Exception { public void testSessionsAreCleanedUpOnThreadedSocketAcceptor() throws Exception { Acceptor acceptor = null; try { - TestAcceptorApplication testAcceptorApplication = new TestAcceptorApplication(); - acceptor = createAcceptorThreaded(testAcceptorApplication); + TestConnectorApplication testAcceptorApplication = new TestConnectorApplication(); + final int port = AvailablePortFinder.getNextAvailable(); + acceptor = createAcceptorThreaded(testAcceptorApplication, port); acceptor.start(); assertEquals(1, acceptor.getSessions().size() ); assertEquals(1 + SESSION_COUNT, Session.numSessions() ); @@ -190,6 +209,82 @@ public void testSessionsAreCleanedUpOnThreadedSocketAcceptor() throws Exception } } + @Test + public void testAcceptorContinueInitializationOnError() throws ConfigError, InterruptedException, IOException { + final int port = AvailablePortFinder.getNextAvailable(); + final int port2 = AvailablePortFinder.getNextAvailable(); + final SessionSettings settings = new SessionSettings(); + final SessionID sessionId = new SessionID("FIX.4.4", "SENDER", "TARGET"); + final SessionID sessionId2 = new SessionID("FIX.4.4", "FOO", "BAR"); + final SessionID sessionId3 = new SessionID("FIX.4.4", "BAR", "BAZ"); + settings.setString(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR, "Y"); + settings.setString("ConnectionType", "acceptor"); + settings.setString("StartTime", "00:00:00"); + settings.setString("EndTime", "00:00:00"); + settings.setString("HeartBtInt", "30"); + settings.setString("BeginString", "FIX.4.4"); + settings.setLong(sessionId, "SocketAcceptPort", port); + settings.setLong(sessionId2, "SocketAcceptPort", port2); + settings.setLong(sessionId3, "SocketAcceptPort", port2); + settings.setString(sessionId, SSLSupport.SETTING_USE_SSL, "Y"); + settings.setString(sessionId, SSLSupport.SETTING_KEY_STORE_NAME, "test.keystore"); + // supply a wrong password to make initialization fail + settings.setString(sessionId, SSLSupport.SETTING_KEY_STORE_PWD, "wrong-password"); + // supply a wrong protocol to make initialization fail + settings.setString(sessionId3, "SocketAcceptProtocol", "foobar"); + + final SocketAcceptor acceptor = new SocketAcceptor(new ApplicationAdapter(), new MemoryStoreFactory(), settings, + new ScreenLogFactory(settings), new DefaultMessageFactory()); + acceptor.start(); + + for (IoAcceptor endpoint : acceptor.getEndpoints()) { + boolean containsFIXCodec = endpoint.getFilterChain().contains(FIXProtocolCodecFactory.FILTER_NAME); + if (endpoint.getLocalAddress() == null) { // failing session is not bound! + assertFalse(containsFIXCodec); + } else { + assertTrue(containsFIXCodec); + } + } + + // sessionid1 is present since it fails after the setup phase + assertTrue(acceptor.getSessions().contains(sessionId)); + // sessionid2 is set up normally + assertTrue(acceptor.getSessions().contains(sessionId2)); + // sessionid3 could not be set up due to problems in the config itself + assertFalse(acceptor.getSessions().contains(sessionId3)); + + acceptor.stop(); + } + + /** + * Ensure that an Acceptor can be started that only has a template session. + */ + @Test + public void testAcceptorTemplate() throws ConfigError, InterruptedException, IOException { + final int port = AvailablePortFinder.getNextAvailable(); + final SessionSettings settings = new SessionSettings(); + final SessionID sessionId = new SessionID("FIX.4.4", "SENDER", "TARGET"); + settings.setString("ConnectionType", "acceptor"); + settings.setString("StartTime", "00:00:00"); + settings.setString("EndTime", "00:00:00"); + settings.setString("HeartBtInt", "30"); + settings.setString("BeginString", "FIX.4.4"); + settings.setLong(sessionId, "SocketAcceptPort", port); + settings.setString(sessionId, Acceptor.SETTING_ACCEPTOR_TEMPLATE, "Y"); + + final SocketAcceptor acceptor = new SocketAcceptor(new ApplicationAdapter(), new MemoryStoreFactory(), settings, + new ScreenLogFactory(settings), new DefaultMessageFactory()); + acceptor.start(); + + for (IoAcceptor endpoint : acceptor.getEndpoints()) { + boolean containsFIXCodec = endpoint.getFilterChain().contains(FIXProtocolCodecFactory.FILTER_NAME); + assertTrue(containsFIXCodec); + } + + acceptor.stop(); + } + + private void checkThreads(ThreadMXBean bean, int expectedNum) { ThreadInfo[] dumpAllThreads = bean.dumpAllThreads(false, false); int qfjMPThreads = 0; @@ -206,12 +301,12 @@ private Session lookupSession(SessionID sessionID) { return Session.lookupSession(sessionID); } - private static class TestAcceptorApplication extends ApplicationAdapter { + private class TestConnectorApplication extends ApplicationAdapter { private final CountDownLatch logonLatch; private final CountDownLatch logoutLatch; - public TestAcceptorApplication() { + public TestConnectorApplication() { logonLatch = new CountDownLatch(1); logoutLatch = new CountDownLatch(1); } @@ -232,7 +327,11 @@ public void waitForLogon() { public void waitForLogout() { try { - assertTrue("Logout timed out", logoutLatch.await(10, TimeUnit.SECONDS)); + final boolean await = logoutLatch.await(10, TimeUnit.SECONDS); + if (!await) { + StackTraceUtil.dumpStackTraces(log); + } + assertTrue("Logout timed out", await); } catch (InterruptedException e) { fail(e.getMessage()); } @@ -248,56 +347,18 @@ public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound // ignore } } - } - - private static class TestInitiatorApplication extends ApplicationAdapter { - - private final CountDownLatch logonLatch; - private final CountDownLatch logoutLatch; - - public TestInitiatorApplication() { - logonLatch = new CountDownLatch(1); - logoutLatch = new CountDownLatch(1); - } - - @Override - public void onLogon(SessionID sessionId) { - super.onLogon(sessionId); - logonLatch.countDown(); - } - - public void waitForLogon() { - try { - assertTrue("Logon timed out", logonLatch.await(10, TimeUnit.SECONDS)); - } catch (InterruptedException e) { - fail(e.getMessage()); - } - } - - public void waitForLogout() { - try { - assertTrue("Logout timed out", logoutLatch.await(10, TimeUnit.SECONDS)); - } catch (InterruptedException e) { - fail(e.getMessage()); - } - } @Override - public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { - try { - if (MsgType.LOGOUT.equals(MessageUtils.getMessageType(message.toString()))) { - logoutLatch.countDown(); - } - } catch (InvalidMessage ex) { - // ignore - } + public void toAdmin(Message message, SessionID sessionId) { + log.info("toAdmin: [{}] {}", sessionId, message); } } - private Acceptor createAcceptor(TestAcceptorApplication testAcceptorApplication) + + private Acceptor createAcceptor(TestConnectorApplication testAcceptorApplication, int port) throws ConfigError { - SessionSettings settings = createAcceptorSettings(); + SessionSettings settings = createAcceptorSettings(port); MessageStoreFactory factory = new MemoryStoreFactory(); quickfix.LogFactory logFactory = new SLF4JLogFactory(new SessionSettings()); @@ -305,10 +366,10 @@ private Acceptor createAcceptor(TestAcceptorApplication testAcceptorApplication) new DefaultMessageFactory()); } - private Acceptor createAcceptorThreaded(TestAcceptorApplication testAcceptorApplication) + private Acceptor createAcceptorThreaded(TestConnectorApplication testAcceptorApplication, int port) throws ConfigError { - SessionSettings settings = createAcceptorSettings(); + SessionSettings settings = createAcceptorSettings(port); MessageStoreFactory factory = new MemoryStoreFactory(); quickfix.LogFactory logFactory = new SLF4JLogFactory(new SessionSettings()); @@ -316,20 +377,21 @@ private Acceptor createAcceptorThreaded(TestAcceptorApplication testAcceptorAppl new DefaultMessageFactory()); } - private SessionSettings createAcceptorSettings() { + private SessionSettings createAcceptorSettings(int socketAcceptPort) { SessionSettings settings = new SessionSettings(); HashMap defaults = new HashMap<>(); defaults.put("ConnectionType", "acceptor"); defaults.put("StartTime", "00:00:00"); defaults.put("EndTime", "00:00:00"); defaults.put("BeginString", "FIX.4.2"); - settings.setString(acceptorSessionID, "SocketAcceptProtocol", ProtocolFactory.getTypeString(ProtocolFactory.VM_PIPE)); - settings.setString(acceptorSessionID, "SocketAcceptPort", "10000"); + defaults.put("NonStopSession", "Y"); + settings.setString(acceptorSessionID, "SocketAcceptProtocol", ProtocolFactory.getTypeString(ProtocolFactory.SOCKET)); + settings.setString(acceptorSessionID, "SocketAcceptPort", String.valueOf(socketAcceptPort)); settings.set(defaults); return settings; } - private Initiator createInitiator(TestInitiatorApplication testInitiatorApplication) throws ConfigError { + private Initiator createInitiator(TestConnectorApplication testInitiatorApplication, int socketConnectPort) throws ConfigError { SessionSettings settings = new SessionSettings(); HashMap defaults = new HashMap<>(); defaults.put("ConnectionType", "initiator"); @@ -339,10 +401,11 @@ private Initiator createInitiator(TestInitiatorApplication testInitiatorApplicat defaults.put("ReconnectInterval", "2"); defaults.put("FileStorePath", "target/data/client"); defaults.put("ValidateUserDefinedFields", "Y"); + defaults.put("NonStopSession", "Y"); settings.setString("BeginString", FixVersions.BEGINSTRING_FIX42); - settings.setString(initiatorSessionID, "SocketConnectProtocol", ProtocolFactory.getTypeString(ProtocolFactory.VM_PIPE)); + settings.setString(initiatorSessionID, "SocketConnectProtocol", ProtocolFactory.getTypeString(ProtocolFactory.SOCKET)); settings.setString(initiatorSessionID, "SocketConnectHost", "127.0.0.1"); - settings.setString(initiatorSessionID, "SocketConnectPort", "10000"); + settings.setString(initiatorSessionID, "SocketConnectPort", String.valueOf(socketConnectPort)); settings.set(defaults); MessageStoreFactory factory = new MemoryStoreFactory(); diff --git a/quickfixj-core/src/test/java/quickfix/SocketInitiatorTest.java b/quickfixj-core/src/test/java/quickfix/SocketInitiatorTest.java index 87cf2d2737..89a0de8637 100644 --- a/quickfixj-core/src/test/java/quickfix/SocketInitiatorTest.java +++ b/quickfixj-core/src/test/java/quickfix/SocketInitiatorTest.java @@ -22,13 +22,19 @@ import org.apache.mina.core.filterchain.IoFilterAdapter; import org.apache.mina.core.session.IoSession; import org.apache.mina.core.write.WriteRequest; +import org.apache.mina.util.AvailablePortFinder; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import quickfix.field.MsgType; import quickfix.mina.ProtocolFactory; import quickfix.mina.SingleThreadedEventHandlingStrategy; +import quickfix.mina.initiator.ConnectException; +import quickfix.mina.ssl.SSLSupport; import quickfix.test.acceptance.ATServer; +import quickfix.test.util.StackTraceUtil; import java.io.File; import java.io.IOException; @@ -36,8 +42,10 @@ import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; +import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; +import java.net.SocketAddress; import java.util.HashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -45,16 +53,15 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; -import static junit.framework.TestCase.assertNotNull; -import org.apache.mina.util.AvailablePortFinder; -import org.junit.After; +import static junit.framework.TestCase.assertNotNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import quickfix.field.MsgType; -import quickfix.test.util.ReflectionUtil; + + public class SocketInitiatorTest { private final Logger log = LoggerFactory.getLogger(getClass()); @@ -229,6 +236,52 @@ public void testDoubleStartOfInitiator() throws Exception { } } + @Test + public void testInitiatorConnectException() throws Exception { + // use a free port to make sure nothing is listening + int freePort = AvailablePortFinder.getNextAvailable(); + Initiator initiator = null; + String host = "localhost"; + AtomicBoolean onConnectExceptionWasCalled = new AtomicBoolean(false); + AtomicReference socketAddress = new AtomicReference<>(null); + try { + SessionID clientSessionID = new SessionID(FixVersions.BEGINSTRING_FIX42, "TW", "ISLD"); + SessionSettings settings = getClientSessionSettings(clientSessionID, freePort); + settings.setString(clientSessionID, "ReconnectInterval", "1"); + settings.setString(clientSessionID, "SocketConnectHost", host); + settings.setString(clientSessionID, "SocketConnectProtocol", ProtocolFactory.getTypeString(ProtocolFactory.SOCKET)); + + SessionStateListener sessionStateListener = new SessionStateListener() { + @Override + public void onConnectException(SessionID sessionID, Exception e) { + onConnectExceptionWasCalled.set(true); + socketAddress.set(((ConnectException) e).getSocketAddress()); + } + }; + // add state listener on creation of Session + Application clientApplication = new ApplicationAdapter() { + @Override + public void onCreate(SessionID sessionId) { + Session.lookupSession(clientSessionID).addStateListener(sessionStateListener); + } + }; + DefaultSessionFactory sessionFactory = new DefaultSessionFactory(clientApplication, new MemoryStoreFactory(), new ScreenLogFactory(settings), new DefaultMessageFactory()); + initiator = new SocketInitiator(sessionFactory, settings, 10000); + initiator.start(); + Thread.sleep(5000); // make sure we try to connect + } finally { + if (initiator != null) { + initiator.stop(true); + } + assertTrue(onConnectExceptionWasCalled.get()); + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress.get(); + assertNotNull(inetSocketAddress); + assertEquals(host, inetSocketAddress.getHostName()); + assertEquals(freePort, inetSocketAddress.getPort()); + } + } + + private interface LogSessionStateListener extends Log, SessionStateListener {} // QFJ-907 @@ -264,6 +317,7 @@ public void testConnectedSocketsAreClosedAfterInitiatorClosed() throws Exception socketThread.start(); final SessionSettings settings = new SessionSettings(); + settings.setString("NonStopSession", "Y"); settings.setString("StartTime", "00:00:00"); settings.setString("EndTime", "00:00:00"); settings.setString("ReconnectInterval", "30"); @@ -301,38 +355,14 @@ public void onErrorEvent(String text) { } @Override - public void onConnect() { + public void onConnect(SessionID sessionID) { onConnectCallCount.incrementAndGet(); } @Override - public void onDisconnect() { + public void onDisconnect(SessionID sessionID) { onDisconnectCallCount.incrementAndGet(); } - - @Override - public void onLogon() { - } - - @Override - public void onLogout() { - } - - @Override - public void onReset() { - } - - @Override - public void onRefresh() { - } - - @Override - public void onMissedHeartBeat() { - } - - @Override - public void onHeartBeatTimeout() { - } }; LogFactory logFactory = sessionID -> logSessionStateListener; @@ -354,6 +384,37 @@ public void onHeartBeatTimeout() { assertEquals(1, onDisconnectCallCount.intValue()); } + + @Test + public void testInitiatorContinueInitializationOnError() throws ConfigError, InterruptedException, IOException { + final ServerSocket serverSocket = new ServerSocket(0); + final int port = serverSocket.getLocalPort(); + final SessionSettings settings = new SessionSettings(); + final SessionID sessionId = new SessionID("FIX.4.4", "SENDER", "TARGET"); + settings.setString(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR, "Y"); + settings.setString(sessionId, "BeginString", "FIX.4.4"); + settings.setString("ConnectionType", "initiator"); + settings.setLong(sessionId, "SocketConnectPort", port); + settings.setString(sessionId, "SocketConnectHost", "localhost"); + settings.setString("NonStopSession", "Y"); + settings.setString("StartTime", "00:00:00"); + settings.setString("EndTime", "00:00:00"); + settings.setString("HeartBtInt", "30"); + settings.setString("SocketConnectProtocol", ProtocolFactory.getTypeString(ProtocolFactory.SOCKET)); + settings.setString(sessionId, SSLSupport.SETTING_USE_SSL, "Y"); + settings.setString(sessionId, SSLSupport.SETTING_KEY_STORE_NAME, "test.keystore"); + // supply a wrong password to make initialization fail + settings.setString(sessionId, SSLSupport.SETTING_KEY_STORE_PWD, "wrong-password"); + + final SocketInitiator initiator = new SocketInitiator(new ApplicationAdapter(), new MemoryStoreFactory(), settings, + new ScreenLogFactory(settings), new DefaultMessageFactory()); + initiator.start(); + + assertTrue(initiator.getInitiators().isEmpty()); + initiator.stop(); + } + + private void doTestOfRestart(SessionID clientSessionID, ClientApplication clientApplication, final Initiator initiator, File messageLog, int port) throws InterruptedException, ConfigError { ServerThread serverThread = new ServerThread(port); @@ -362,13 +423,14 @@ private void doTestOfRestart(SessionID clientSessionID, ClientApplication client serverThread.start(); serverThread.waitForInitialization(); long messageLogLength = 0; + Session clientSession = null; try { clientApplication.setUpLogonExpectation(); initiator.start(); assertTrue(initiator.getSessions().contains(clientSessionID)); assertEquals(1, initiator.getSessions().size()); - Session clientSession = Session.lookupSession(clientSessionID); + clientSession = Session.lookupSession(clientSessionID); assertLoggedOn(clientApplication, clientSession); clientApplication.setUpLogoutExpectation(); @@ -394,8 +456,13 @@ private void doTestOfRestart(SessionID clientSessionID, ClientApplication client // QFJ-698: check that we were still able to write to the messageLog after the restart assertTrue(messageLog.length() > messageLogLength); } + + clientApplication.setUpLogoutExpectation(); } finally { initiator.stop(); + if (clientSession != null) { + assertLoggedOut(clientApplication, clientSession); + } } } finally { serverThread.interrupt(); @@ -443,6 +510,7 @@ private SessionSettings getClientSessionSettings(SessionID clientSessionID, int defaults.put("SocketConnectProtocol", ProtocolFactory.getTypeString(ProtocolFactory.VM_PIPE)); defaults.put("SocketConnectHost", "localhost"); defaults.put("SocketConnectPort", Integer.toString(port)); + defaults.put("NonStopSession", "Y"); defaults.put("StartTime", "00:00:00"); defaults.put("EndTime", "00:00:00"); defaults.put("HeartBtInt", "30"); @@ -470,7 +538,7 @@ private void assertLoggedOn(ClientApplication clientApplication, Session clientS } if ( clientApplication.logonLatch.getCount() > 0 ) { System.err.println("XXX Dumping threads since latch count is not zero..."); - ReflectionUtil.dumpStackTraces(); + StackTraceUtil.dumpStackTraces(); } }); } finally { @@ -487,7 +555,7 @@ private void assertLoggedOut(ClientApplication clientApplication, Session client assertNotNull("no client session", clientSession); final boolean await = clientApplication.logoutLatch.await(20, TimeUnit.SECONDS); if (!await) { - ReflectionUtil.dumpStackTraces(); + StackTraceUtil.dumpStackTraces(); } assertTrue("Expected logout did not occur", await); assertFalse("client session logged in?", clientSession.isLoggedOn()); @@ -527,11 +595,11 @@ public void onLogon(SessionID sessionId) { @Override public void toAdmin(Message message, SessionID sessionId) { - log.info("[{}] {}", sessionId, message); + log.info("toAdmin: [{}] {}", sessionId, message); // Only countdown the latch if a logout message is actually sent try { - if (logoutLatch != null && message.getHeader().isSetField(MsgType.FIELD) + if (logoutLatch != null && logoutLatch.getCount() > 0 && message.getHeader().isSetField(MsgType.FIELD) && MsgType.LOGOUT.equals(message.getHeader().getString(MsgType.FIELD))) { log.info("Releasing logout latch for session [{}] with message {}", sessionId, message); logoutLatch.countDown(); diff --git a/quickfixj-core/src/test/java/quickfix/UnitTestApplication.java b/quickfixj-core/src/test/java/quickfix/UnitTestApplication.java index aad614aff2..656dbc5b92 100644 --- a/quickfixj-core/src/test/java/quickfix/UnitTestApplication.java +++ b/quickfixj-core/src/test/java/quickfix/UnitTestApplication.java @@ -23,58 +23,69 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class UnitTestApplication implements ApplicationExtended, SessionStateListener { + private final Logger log = LoggerFactory.getLogger(UnitTestApplication.class); - public final List fromAppMessages = new ArrayList<>(); - public final List toAppMessages = new ArrayList<>(); - public final List fromAdminMessages = new ArrayList<>(); - public final List toAdminMessages = new ArrayList<>(); - public final List logonSessions = new ArrayList<>(); - public final List logoutSessions = new ArrayList<>(); - public final List createSessions = new ArrayList<>(); + public final List fromAppMessages = Collections.synchronizedList(new ArrayList<>()); + public final List toAppMessages = Collections.synchronizedList(new ArrayList<>()); + public final List fromAdminMessages = Collections.synchronizedList(new ArrayList<>()); + public final List toAdminMessages = Collections.synchronizedList(new ArrayList<>()); + public final List logonSessions = Collections.synchronizedList(new ArrayList<>()); + public final List logoutSessions = Collections.synchronizedList(new ArrayList<>()); + public final List createSessions = Collections.synchronizedList(new ArrayList<>()); public int sessionResets = 0; + @Override public boolean canLogon(SessionID sessionID) { return true; } + @Override public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { log.info("from app [{}] {}", sessionId, message); fromAppMessages.add(message); } + @Override public void toApp(Message message, SessionID sessionId) throws DoNotSend { log.info("to app [{}] {}", sessionId, message); toAppMessages.add(message); } + @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { log.info("from admin [{}] {}", sessionId, message); fromAdminMessages.add(message); } + @Override public void toAdmin(Message message, SessionID sessionId) { log.info("to admin [{}] {}", sessionId, message); toAdminMessages.add(message); } + @Override public void onBeforeSessionReset(SessionID sessionId) { log.info("onBeforeSessionReset [{}]", sessionId); } + @Override public void onLogout(SessionID sessionId) { logoutSessions.add(sessionId); } + @Override public void onLogon(SessionID sessionId) { logonSessions.add(sessionId); } + @Override public void onCreate(SessionID sessionId) { createSessions.add(sessionId); } @@ -90,52 +101,35 @@ public void clear() { } public Message lastFromAppMessage() { - if (fromAppMessages.isEmpty()) + if (fromAppMessages.isEmpty()) { return null; + } return fromAppMessages.get(fromAppMessages.size() - 1); } public Message lastFromAdminMessage() { - if (fromAdminMessages.isEmpty()) + if (fromAdminMessages.isEmpty()) { return null; + } return fromAdminMessages.get(fromAdminMessages.size() - 1); } public Message lastToAppMessage() { - if (toAppMessages.isEmpty()) + if (toAppMessages.isEmpty()) { return null; + } return toAppMessages.get(toAppMessages.size() - 1); } public Message lastToAdminMessage() { - if (toAdminMessages.isEmpty()) + if (toAdminMessages.isEmpty()) { return null; + } return toAdminMessages.get(toAdminMessages.size() - 1); } - public void onConnect() { - } - - public void onDisconnect() { - } - - public void onLogon() { - } - - public void onLogout() { - } - - public void onReset() { + @Override + public void onReset(SessionID sessionID) { sessionResets++; } - - public void onRefresh() { - } - - public void onMissedHeartBeat() { - } - - public void onHeartBeatTimeout() { - } - } diff --git a/quickfixj-core/src/test/java/quickfix/mina/IoSessionResponderTest.java b/quickfixj-core/src/test/java/quickfix/mina/IoSessionResponderTest.java index 0ee62f5d28..77d2ac6546 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/IoSessionResponderTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/IoSessionResponderTest.java @@ -19,20 +19,30 @@ package quickfix.mina; -import junit.framework.TestCase; +import java.net.InetSocketAddress; import org.apache.mina.core.future.WriteFuture; import org.apache.mina.core.session.IoSession; -import java.net.InetSocketAddress; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; -import static org.mockito.Mockito.*; -public class IoSessionResponderTest extends TestCase { +public class IoSessionResponderTest { + + @Test public void testAsynchronousSend() throws Exception { IoSession mockIoSession = mock(IoSession.class); WriteFuture mockWriteFuture = mock(WriteFuture.class); - stub(mockWriteFuture.isWritten()).toReturn(true); - stub(mockIoSession.write("abcd")).toReturn(mockWriteFuture); + when(mockWriteFuture.isWritten()).thenReturn(true); + when(mockIoSession.write("abcd")).thenReturn(mockWriteFuture); IoSessionResponder responder = new IoSessionResponder(mockIoSession, false, 0, 0); boolean result = responder.send("abcd"); @@ -43,12 +53,13 @@ public void testAsynchronousSend() throws Exception { verifyNoMoreInteractions(mockIoSession); } + @Test public void testSynchronousSend() throws Exception { int timeout = 123; IoSession mockIoSession = mock(IoSession.class); WriteFuture mockWriteFuture = mock(WriteFuture.class); - stub(mockIoSession.write("abcd")).toReturn(mockWriteFuture); - stub(mockWriteFuture.awaitUninterruptibly(timeout)).toReturn(true); + when(mockIoSession.write("abcd")).thenReturn(mockWriteFuture); + when(mockWriteFuture.awaitUninterruptibly(timeout)).thenReturn(true); IoSessionResponder responder = new IoSessionResponder(mockIoSession, true, timeout, 0); boolean result = responder.send("abcd"); @@ -60,12 +71,13 @@ public void testSynchronousSend() throws Exception { verifyNoMoreInteractions(mockIoSession); } + @Test public void testSynchronousSendWithJoinException() throws Exception { int timeout = 123; IoSession mockIoSession = mock(IoSession.class); WriteFuture mockWriteFuture = mock(WriteFuture.class); - stub(mockIoSession.write("abcd")).toReturn(mockWriteFuture); + when(mockIoSession.write("abcd")).thenReturn(mockWriteFuture); doThrow(new RuntimeException("TEST")).when(mockWriteFuture).awaitUninterruptibly(timeout); IoSessionResponder responder = new IoSessionResponder(mockIoSession, true, timeout, 0); @@ -78,13 +90,14 @@ public void testSynchronousSendWithJoinException() throws Exception { verifyNoMoreInteractions(mockIoSession); } + @Test public void testSynchronousSendWithJoinTimeout() throws Exception { int timeout = 123; IoSession mockIoSession = mock(IoSession.class); WriteFuture mockWriteFuture = mock(WriteFuture.class); - stub(mockIoSession.write("abcd")).toReturn(mockWriteFuture); - stub(mockWriteFuture.awaitUninterruptibly(timeout)).toReturn(false); + when(mockIoSession.write("abcd")).thenReturn(mockWriteFuture); + when(mockWriteFuture.awaitUninterruptibly(timeout)).thenReturn(false); IoSessionResponder responder = new IoSessionResponder(mockIoSession, true, timeout, 0); boolean result = responder.send("abcd"); @@ -96,10 +109,12 @@ public void testSynchronousSendWithJoinTimeout() throws Exception { verifyNoMoreInteractions(mockIoSession); } + @Test public void testDisconnect() throws Exception { IoSession mockProtocolSession = mock(IoSession.class); - stub(mockProtocolSession.getScheduledWriteMessages()).toReturn(0); - stub(mockProtocolSession.closeNow()).toReturn(null); + + when(mockProtocolSession.getScheduledWriteMessages()).thenReturn(0); + when(mockProtocolSession.closeNow()).thenReturn(null); IoSessionResponder responder = new IoSessionResponder(mockProtocolSession, false, 0, 0); responder.disconnect(); @@ -110,10 +125,10 @@ public void testDisconnect() throws Exception { verifyNoMoreInteractions(mockProtocolSession); } + @Test public void testGetRemoteSocketAddress() throws Exception { IoSession mockProtocolSession = mock(IoSession.class); - stub(mockProtocolSession.getRemoteAddress()).toReturn( - new InetSocketAddress("1.2.3.4", 5432)); + when(mockProtocolSession.getRemoteAddress()).thenReturn(new InetSocketAddress("1.2.3.4", 5432)); IoSessionResponder responder = new IoSessionResponder(mockProtocolSession, false, 0, 0); diff --git a/quickfixj-core/src/test/java/quickfix/mina/LostLogoutTest.java b/quickfixj-core/src/test/java/quickfix/mina/LostLogoutTest.java index 63215c1981..be2cc499c8 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/LostLogoutTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/LostLogoutTest.java @@ -1,5 +1,6 @@ package quickfix.mina; +import org.apache.mina.util.AvailablePortFinder; import org.junit.Test; import quickfix.Application; import quickfix.DefaultMessageFactory; @@ -51,11 +52,12 @@ public class LostLogoutTest { @Test public void lostLogoutMessageTest() throws Exception { + final int port = AvailablePortFinder.getNextAvailable(); // create server (acceptor) - server = new ServerApp(); + server = new ServerApp(port); // create client (initiator) and start the FIX session (log on) - client = new ClientApp(); + client = new ClientApp(port); // wait until until client is logged on client.waitUntilLoggedOn(); @@ -89,11 +91,11 @@ private class ServerApp implements Application { private SocketAcceptor acceptor = null; private final SessionID sid = new SessionID("FIX.4.4", "SERVER", "CLIENT"); - public ServerApp() throws Exception { + public ServerApp(int port) throws Exception { SessionSettings settings = new SessionSettings(); settings.setString("ConnectionType", "acceptor"); settings.setString("SocketAcceptAddress", "127.0.0.1"); - settings.setLong("SocketAcceptPort", 54321); + settings.setLong("SocketAcceptPort", port); settings.setString("StartTime", "00:00:00"); settings.setString("EndTime", "00:00:00"); settings.setString("UseDataDictionary", "N"); @@ -144,7 +146,7 @@ public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, try { Thread.sleep(1000); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); } System.out.println("Server: message processing end"); } @@ -164,12 +166,12 @@ private static class ClientApp implements Application { private SocketInitiator initiator = null; private Session session; - public ClientApp() throws Exception { + public ClientApp(int port) throws Exception { SessionID sid = new SessionID("FIX.4.4", "CLIENT", "SERVER"); SessionSettings settings = new SessionSettings(); settings.setString("ConnectionType", "initiator"); settings.setString("SocketConnectHost", "127.0.0.1"); - settings.setLong("SocketConnectPort", 54321); + settings.setLong("SocketConnectPort", port); settings.setString("StartTime", "00:00:00"); settings.setString("EndTime", "00:00:00"); settings.setLong("HeartBtInt", 30); diff --git a/quickfixj-core/src/test/java/quickfix/mina/LostLogoutThreadedTest.java b/quickfixj-core/src/test/java/quickfix/mina/LostLogoutThreadedTest.java index 1f13240842..053c08d4be 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/LostLogoutThreadedTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/LostLogoutThreadedTest.java @@ -1,5 +1,6 @@ package quickfix.mina; +import org.apache.mina.util.AvailablePortFinder; import org.junit.Test; import quickfix.Application; import quickfix.DefaultMessageFactory; @@ -51,11 +52,12 @@ public class LostLogoutThreadedTest { @Test public void lostLogoutMessageTest() throws Exception { + final int port = AvailablePortFinder.getNextAvailable(); // create server (acceptor) - server = new ServerApp(); + server = new ServerApp(port); // create client (initiator) and start the FIX session (log on) - client = new ClientApp(); + client = new ClientApp(port); // wait until until client is logged on client.waitUntilLoggedOn(); @@ -88,12 +90,12 @@ public void lostLogoutMessageTest() throws Exception { private class ServerApp implements Application { private ThreadedSocketAcceptor acceptor = null; - public ServerApp() throws Exception { + public ServerApp(int port) throws Exception { SessionID sid = new SessionID("FIX.4.4", "SERVER", "CLIENT"); SessionSettings settings = new SessionSettings(); settings.setString("ConnectionType", "acceptor"); settings.setString("SocketAcceptAddress", "127.0.0.1"); - settings.setLong("SocketAcceptPort", 54321); + settings.setLong("SocketAcceptPort", port); settings.setString("StartTime", "00:00:00"); settings.setString("EndTime", "00:00:00"); settings.setString("UseDataDictionary", "N"); @@ -144,7 +146,7 @@ public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, try { Thread.sleep(1000); } catch (InterruptedException e) { - e.printStackTrace(); + Thread.currentThread().interrupt(); } System.out.println("Server: message processing end"); } @@ -164,12 +166,12 @@ private static class ClientApp implements Application { private ThreadedSocketInitiator initiator = null; private Session session; - public ClientApp() throws Exception { + public ClientApp(int port) throws Exception { SessionID sid = new SessionID("FIX.4.4", "CLIENT", "SERVER"); SessionSettings settings = new SessionSettings(); settings.setString("ConnectionType", "initiator"); settings.setString("SocketConnectHost", "127.0.0.1"); - settings.setLong("SocketConnectPort", 54321); + settings.setLong("SocketConnectPort", port); settings.setString("StartTime", "00:00:00"); settings.setString("EndTime", "00:00:00"); settings.setLong("HeartBtInt", 30); diff --git a/quickfixj-core/src/test/java/quickfix/mina/ProtocolFactoryTest.java b/quickfixj-core/src/test/java/quickfix/mina/ProtocolFactoryTest.java new file mode 100644 index 0000000000..e6de3f42e0 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/mina/ProtocolFactoryTest.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.mina; + +import org.apache.mina.core.service.IoConnector; +import org.apache.mina.proxy.ProxyConnector; +import org.apache.mina.proxy.session.ProxyIoSession; +import org.apache.mina.transport.socket.SocketConnector; +import org.apache.mina.util.AvailablePortFinder; +import org.junit.Test; +import quickfix.ConfigError; + +import java.net.InetSocketAddress; + +import static org.junit.Assert.assertNull; + +public class ProtocolFactoryTest { + + @Test + public void shouldCreateProxyConnectorWithoutPreferredAuthOrder() throws ConfigError { + InetSocketAddress address = new InetSocketAddress(AvailablePortFinder.getNextAvailable()); + InetSocketAddress proxyAddress = new InetSocketAddress(AvailablePortFinder.getNextAvailable()); + + IoConnector connector = ProtocolFactory.createIoConnector(address); + ProxyConnector proxyConnector = ProtocolFactory + .createIoProxyConnector((SocketConnector) connector, address, proxyAddress, "http", "1.0", "user", + "password", "domain", "workstation"); + + ProxyIoSession proxySession = proxyConnector.getProxyIoSession(); + assertNull(proxySession.getPreferedOrder()); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/mina/SessionConnectorTest.java b/quickfixj-core/src/test/java/quickfix/mina/SessionConnectorTest.java index ec364dee15..72ccf51121 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/SessionConnectorTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/SessionConnectorTest.java @@ -99,8 +99,8 @@ public void testConnector() throws Exception { assertTrue(session.isEnabled()); connector.logoutAllSessions(true); - // Acceptors should get re-enabled after Logout - assertTrue(session.isEnabled()); + // Acceptors should not get re-enabled after initiating Logout + assertFalse(session.isEnabled()); assertEquals(9999, connector.getIntSetting(Acceptor.SETTING_SOCKET_ACCEPT_PORT)); @@ -312,7 +312,7 @@ public void testConcurrentAccess() throws ConfigError, InterruptedException { Thread.sleep(randomSleep); } catch (final Throwable throwable) { throwable.printStackTrace(); - fail("Well.. this operation shouldnt fail"); + fail("This operation shouldn't fail"); } finally { countDownLatch.countDown(); } diff --git a/quickfixj-core/src/test/java/quickfix/mina/SingleThreadedEventHandlingStrategyTest.java b/quickfixj-core/src/test/java/quickfix/mina/SingleThreadedEventHandlingStrategyTest.java index 8802783564..7ca548f20c 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/SingleThreadedEventHandlingStrategyTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/SingleThreadedEventHandlingStrategyTest.java @@ -68,7 +68,7 @@ public void cleanup() { try { Thread.sleep(500); } catch (InterruptedException ex) { - // ignored + Thread.currentThread().interrupt(); } } } @@ -373,7 +373,7 @@ private static void assertQFJMessageProcessorThreads(int expected) { try { Thread.sleep(100); } catch (InterruptedException ex) { - // ignored + Thread.currentThread().interrupt(); } dumpAllThreads = bean.dumpAllThreads(false, false); qfjMPThreads = getMessageProcessorThreads(dumpAllThreads); diff --git a/quickfixj-core/src/test/java/quickfix/mina/WatermarkTrackerTest.java b/quickfixj-core/src/test/java/quickfix/mina/WatermarkTrackerTest.java index fbb6d9ccfc..4f5f770a5a 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/WatermarkTrackerTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/WatermarkTrackerTest.java @@ -23,7 +23,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; diff --git a/quickfixj-core/src/test/java/quickfix/mina/acceptor/AcceptorIoHandlerTest.java b/quickfixj-core/src/test/java/quickfix/mina/acceptor/AcceptorIoHandlerTest.java index a7186d4185..15405da74c 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/acceptor/AcceptorIoHandlerTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/acceptor/AcceptorIoHandlerTest.java @@ -57,9 +57,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.stub; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; public class AcceptorIoHandlerTest { @@ -72,19 +72,20 @@ public class AcceptorIoHandlerTest { public void testFIXTLogonAndApplVerID() throws Exception { EventHandlingStrategy mockEventHandlingStrategy = mock(EventHandlingStrategy.class); IoSession mockIoSession = mock(IoSession.class); + SessionSettings settings = mock(SessionSettings.class); final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIXT11, "SENDER", "TARGET"); try (Session session = SessionFactoryTestSupport.createSession(sessionID, new UnitTestApplication(), false)) { - stub(mockIoSession.getAttribute("QF_SESSION")).toReturn(null); // to create a new Session + when(mockIoSession.getAttribute("QF_SESSION")).thenReturn(null); // to create a new Session final HashMap acceptorSessions = new HashMap<>(); acceptorSessions.put(sessionID, session); final StaticAcceptorSessionProvider sessionProvider = createSessionProvider(acceptorSessions); final AcceptorIoHandler handler = new AcceptorIoHandler(sessionProvider, - new NetworkingOptions(new Properties()), mockEventHandlingStrategy); + settings, new NetworkingOptions(new Properties()), mockEventHandlingStrategy); final DefaultApplVerID defaultApplVerID = new DefaultApplVerID(ApplVerID.FIX50SP2); final Logon message = new Logon(new EncryptMethod(EncryptMethod.NONE_OTHER), @@ -103,14 +104,15 @@ public void testFIXTLogonAndApplVerID() throws Exception { @Test public void testMessageBeforeLogon() throws Exception { IoSession mockIoSession = mock(IoSession.class); - stub(mockIoSession.getAttribute("QF_SESSION")).toReturn(null); + SessionSettings settings = mock(SessionSettings.class); + when(mockIoSession.getAttribute("QF_SESSION")).thenReturn(null); EventHandlingStrategy mockEventHandlingStrategy = mock(EventHandlingStrategy.class); HashMap acceptorSessions = new HashMap<>(); AcceptorIoHandler handler = new AcceptorIoHandler(createSessionProvider(acceptorSessions), - new NetworkingOptions(new Properties()), mockEventHandlingStrategy); + settings, new NetworkingOptions(new Properties()), mockEventHandlingStrategy); handler.processMessage(mockIoSession, new Logout()); @@ -125,9 +127,10 @@ private StaticAcceptorSessionProvider createSessionProvider(HashMap acceptorSessions = new HashMap<>(); AcceptorIoHandler handler = new AcceptorIoHandler(createSessionProvider(acceptorSessions), - new NetworkingOptions(new Properties()), mockEventHandlingStrategy); + settings, new NetworkingOptions(new Properties()), mockEventHandlingStrategy); handler.processMessage(mockIoSession, logout); @@ -152,8 +155,9 @@ public void testMessageBeforeLogonWithBoundSession() throws Exception { @Test public void testMessageBeforeLogonWithKnownButUnboundSession() throws Exception { IoSession mockIoSession = mock(IoSession.class); + SessionSettings settings = mock(SessionSettings.class); - stub(mockIoSession.getAttribute("QF_SESSION")).toReturn(null); + when(mockIoSession.getAttribute("QF_SESSION")).thenReturn(null); EventHandlingStrategy mockEventHandlingStrategy = mock(EventHandlingStrategy.class); @@ -171,7 +175,7 @@ public void testMessageBeforeLogonWithKnownButUnboundSession() throws Exception HashMap acceptorSessions = new HashMap<>(); acceptorSessions.put(qfSession.getSessionID(), qfSession); AcceptorIoHandler handler = new AcceptorIoHandler(createSessionProvider(acceptorSessions), - new NetworkingOptions(new Properties()), mockEventHandlingStrategy); + settings, new NetworkingOptions(new Properties()), mockEventHandlingStrategy); handler.processMessage(mockIoSession, logout); @@ -185,19 +189,20 @@ public void testMessageBeforeLogonWithKnownButUnboundSession() throws Exception public void testLogonWithoutHeartBtInt() throws Exception { EventHandlingStrategy mockEventHandlingStrategy = mock(EventHandlingStrategy.class); IoSession mockIoSession = mock(IoSession.class); + SessionSettings settings = mock(SessionSettings.class); final SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIXT11, "SENDER", "TARGET"); try (Session session = SessionFactoryTestSupport.createSession(sessionID, new UnitTestApplication(), false)) { - stub(mockIoSession.getAttribute("QF_SESSION")).toReturn(null); // to create a new Session + when(mockIoSession.getAttribute("QF_SESSION")).thenReturn(null); // to create a new Session final HashMap acceptorSessions = new HashMap<>(); acceptorSessions.put(sessionID, session); final StaticAcceptorSessionProvider sessionProvider = createSessionProvider(acceptorSessions); final AcceptorIoHandler handler = new AcceptorIoHandler(sessionProvider, - new NetworkingOptions(new Properties()), mockEventHandlingStrategy); + settings, new NetworkingOptions(new Properties()), mockEventHandlingStrategy); final DefaultApplVerID defaultApplVerID = new DefaultApplVerID(ApplVerID.FIX50SP2); final Logon message = new Logon(new EncryptMethod(EncryptMethod.NONE_OTHER), @@ -228,14 +233,14 @@ public void testRejectGarbledMessage() throws Exception { session.setRejectGarbledMessage(true); eventHandlingStrategy.blockInThread(); Responder responder = new UnitTestResponder(); - stub(mockIoSession.getAttribute("QF_SESSION")).toReturn(null); // to create a new Session + when(mockIoSession.getAttribute("QF_SESSION")).thenReturn(null); // to create a new Session final HashMap acceptorSessions = new HashMap<>(); acceptorSessions.put(sessionID, session); final StaticAcceptorSessionProvider sessionProvider = createSessionProvider(acceptorSessions); final AcceptorIoHandler handler = new AcceptorIoHandler(sessionProvider, - new NetworkingOptions(new Properties()), eventHandlingStrategy); + settings, new NetworkingOptions(new Properties()), eventHandlingStrategy); final DefaultApplVerID defaultApplVerID = new DefaultApplVerID(ApplVerID.FIX50SP2); final Logon message = new Logon(new EncryptMethod(EncryptMethod.NONE_OTHER), @@ -252,7 +257,7 @@ public void testRejectGarbledMessage() throws Exception { assertEquals(2, session.getStore().getNextTargetMsgSeqNum()); assertEquals(2, session.getStore().getNextSenderMsgSeqNum()); - stub(mockIoSession.getAttribute("QF_SESSION")).toReturn(session); + when(mockIoSession.getAttribute("QF_SESSION")).thenReturn(session); // garbled: character as group count String fixString = "8=FIXT.1.19=6835=B34=249=TARGET52=20180623-22:06:28.97756=SENDER148=foo33=a10=248"; @@ -331,14 +336,14 @@ public void testRejectGarbledMessageWithoutMsgTypeBeforeSessionIsCreated() throw session.setRejectGarbledMessage(true); eventHandlingStrategy.blockInThread(); Responder responder = new UnitTestResponder(); - stub(mockIoSession.getAttribute("QF_SESSION")).toReturn(null); // to create a new Session + when(mockIoSession.getAttribute("QF_SESSION")).thenReturn(null); // to create a new Session final HashMap acceptorSessions = new HashMap<>(); acceptorSessions.put(sessionID, session); final StaticAcceptorSessionProvider sessionProvider = createSessionProvider(acceptorSessions); final AcceptorIoHandler handler = new AcceptorIoHandler(sessionProvider, - new NetworkingOptions(new Properties()), eventHandlingStrategy); + settings, new NetworkingOptions(new Properties()), eventHandlingStrategy); // garbled: missing msgtype String fixString = "8=FIXT.1.19=6834=349=TARGET52=20180623-22:06:28.97756=SENDER148=foo33=a10=248"; diff --git a/quickfixj-core/src/test/java/quickfix/mina/initiator/InitiatorIoHandlerTest.java b/quickfixj-core/src/test/java/quickfix/mina/initiator/InitiatorIoHandlerTest.java index 0a89504956..0f8067630e 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/initiator/InitiatorIoHandlerTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/initiator/InitiatorIoHandlerTest.java @@ -34,7 +34,7 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.stub; +import static org.mockito.Mockito.when; public class InitiatorIoHandlerTest { @@ -57,10 +57,10 @@ public void testRejectGarbledMessage() throws Exception { ApplVerID.FIX50SP2))) { session.setRejectGarbledMessage(true); eventHandlingStrategy.blockInThread(); - stub(mockIoSession.getAttribute("QF_SESSION")).toReturn(null); // to create a new Session + when(mockIoSession.getAttribute("QF_SESSION")).thenReturn(null); // to create a new Session final InitiatorIoHandler handler = new InitiatorIoHandler(session, - new NetworkingOptions(new Properties()), eventHandlingStrategy); + settings, new NetworkingOptions(new Properties()), eventHandlingStrategy); final DefaultApplVerID defaultApplVerID = new DefaultApplVerID(ApplVerID.FIX50SP2); final Logon message = new Logon(new EncryptMethod(EncryptMethod.NONE_OTHER), @@ -77,7 +77,7 @@ public void testRejectGarbledMessage() throws Exception { assertEquals(2, session.getStore().getNextTargetMsgSeqNum()); assertEquals(2, session.getStore().getNextSenderMsgSeqNum()); - stub(mockIoSession.getAttribute("QF_SESSION")).toReturn(session); + when(mockIoSession.getAttribute("QF_SESSION")).thenReturn(session); // garbled: character as group count String fixString = "8=FIXT.1.19=6835=B34=249=TARGET52=20180623-22:06:28.97756=SENDER148=foo33=a10=248"; diff --git a/quickfixj-core/src/test/java/quickfix/mina/message/FIXMessageDecoderTest.java b/quickfixj-core/src/test/java/quickfix/mina/message/FIXMessageDecoderTest.java index e745904ba7..8d56eb1259 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/message/FIXMessageDecoderTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/message/FIXMessageDecoderTest.java @@ -55,6 +55,7 @@ public class FIXMessageDecoderTest { @Before public void setUp() throws Exception { + CharsetSupport.setDefaultCharset(); decoder = new FIXMessageDecoder(); buffer = IoBuffer.allocate(8192); decoderOutput = new ProtocolDecoderOutputForTest(); @@ -62,17 +63,13 @@ public void setUp() throws Exception { @After public void tearDown() throws Exception { + CharsetSupport.setDefaultCharset(); buffer.clear(); } - @Test + @Test(expected = UnsupportedEncodingException.class) public void testInvalidStringCharset() throws Exception { - try { - decoder = new FIXMessageDecoder("BOGUS"); - fail("no exception thrown"); - } catch (UnsupportedEncodingException e) { - // expected - } + decoder = new FIXMessageDecoder("BOGUS"); } @Test @@ -104,8 +101,6 @@ public void testWesternEuropeanDecoding() throws Exception { doWesternEuropeanDecodingTest(); } catch (InvalidMessage e) { // expected - } finally { - CharsetSupport.setCharset(CharsetSupport.getDefaultCharset()); } } @@ -490,4 +485,30 @@ public void testBadBodyLength() throws Exception { setUpBuffer(message); assertMessageFound(goodMessage); } + + /** + * Several bad messages after each other should not send the decoder in an + * infinite loop. https://github.com/quickfix-j/quickfixj/issues/432 + */ + @Test(timeout = 1000) + public void testLengthFormatError() throws Exception { + String badMessages = "8=FIX.4.4\0019=058=\0018=FIX.4.4\0019=058=\0018=FIX.4.4\0019=058=\0018=FIX.4.4\0019=058=\001"; + String goodMessage = "8=FIX.4.4\0019=12\00135=Y\001108=30\00110=037\001"; + setUpBuffer(badMessages + goodMessage + badMessages + goodMessage); + assertMessageFound(goodMessage, 2); + } + + /** + * Several bad messages after each other should not send the decoder in an + * infinite loop. https://github.com/quickfix-j/quickfixj/issues/432 + */ + @Test(timeout = 1000) + public void testLengthFormatError2() throws Exception { + decoder = new FIXMessageDecoder("UTF-16"); + setUpBuffer("8=FIX.4.2\0019=128=FIX.4.2\0019=8=FIX.4.2\0019=128=" + + "FIX.4.2\0019=8=FIX.4.2\0019=12\00135=X\001108=30\00110=049\001"); + MessageDecoderResult decoderResult = decoder.decode(null, buffer, decoderOutput); + assertEquals("wrong decoder result", MessageDecoderResult.OK, decoderResult); + assertEquals("Wrong encoding", 14397, (int) decoderOutput.getMessage().charAt(0)); + } } diff --git a/quickfixj-core/src/test/java/quickfix/mina/message/FIXMessageEncoderTest.java b/quickfixj-core/src/test/java/quickfix/mina/message/FIXMessageEncoderTest.java index 204ef60b51..b36573dbba 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/message/FIXMessageEncoderTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/message/FIXMessageEncoderTest.java @@ -19,12 +19,13 @@ package quickfix.mina.message; -import java.io.UnsupportedEncodingException; +import org.apache.mina.filter.codec.ProtocolCodecException; -import junit.framework.ComparisonFailure; -import junit.framework.TestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.ComparisonFailure; +import org.junit.Test; -import org.apache.mina.filter.codec.ProtocolCodecException; import org.quickfixj.CharsetSupport; import quickfix.Message; @@ -34,13 +35,25 @@ import quickfix.fix40.Logon; import quickfix.fix44.News; -public class FIXMessageEncoderTest extends TestCase { +import java.io.UnsupportedEncodingException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class FIXMessageEncoderTest { + @Before + public void setUp() throws UnsupportedEncodingException { + CharsetSupport.setDefaultCharset(); + } + + @After public void tearDown() throws UnsupportedEncodingException { - // reset charset after every test - CharsetSupport.setCharset(CharsetSupport.getDefaultCharset()); + CharsetSupport.setDefaultCharset(); } + @Test public void testEncoding() throws Exception { FIXMessageEncoder encoder = new FIXMessageEncoder(); Message message = new Logon(); @@ -51,6 +64,7 @@ public void testEncoding() throws Exception { assertTrue(protocolEncoderOutputForTest.buffer.limit() > 0); } + @Test public void testWesternEuropeanEncoding() throws Exception { String headline = "\u00E4bcf\u00F6d\u00E7\u00E9"; @@ -67,6 +81,7 @@ public void testWesternEuropeanEncoding() throws Exception { } } + @Test public void testChineseEncoding() throws Exception { String headline = "\u6D4B\u9A8C\u6570\u636E"; @@ -96,16 +111,13 @@ private void doEncodingTest(String headline) throws ProtocolCodecException, Unsu assertEquals("wrong encoding", new String(bytes, CharsetSupport.getCharset()), news.toString()); } + @Test(expected = ProtocolCodecException.class) public void testEncodingBadType() throws Exception { FIXMessageEncoder encoder = new FIXMessageEncoder(); - try { - encoder.encode(null, new Object(), new ProtocolEncoderOutputForTest()); - fail("expected exception"); - } catch (ProtocolCodecException e) { - // expected - } + encoder.encode(null, new Object(), new ProtocolEncoderOutputForTest()); } + @Test public void testEncodingStringEnglish() throws Exception { FIXMessageEncoder encoder = new FIXMessageEncoder(); ProtocolEncoderOutputForTest protocolEncoderOutputForTest = new ProtocolEncoderOutputForTest(); @@ -113,6 +125,7 @@ public void testEncodingStringEnglish() throws Exception { assertEquals(4, protocolEncoderOutputForTest.buffer.limit()); } + @Test public void testEncodingStringChinese() throws Exception { CharsetSupport.setCharset("UTF-8"); FIXMessageEncoder encoder = new FIXMessageEncoder(); @@ -120,5 +133,4 @@ public void testEncodingStringChinese() throws Exception { encoder.encode(null, "\u6D4B\u9A8C\u6570\u636E", protocolEncoderOutputForTest); assertEquals(12, protocolEncoderOutputForTest.buffer.limit()); } - } diff --git a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLAndNonSSLTest.java b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLAndNonSSLTest.java index d000fd6568..bc46036b6d 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLAndNonSSLTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLAndNonSSLTest.java @@ -214,6 +214,7 @@ public void run() { shutdownLatch.await(); } catch (InterruptedException e1) { try { + Thread.currentThread().interrupt(); acceptor.stop(true); } catch (RuntimeException e) { e.printStackTrace(); diff --git a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLCertificateTest.java b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLCertificateTest.java index 795e8e4ec6..6d31b9293f 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLCertificateTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLCertificateTest.java @@ -22,6 +22,8 @@ import org.apache.mina.core.filterchain.IoFilterAdapter; import org.apache.mina.core.filterchain.IoFilterChain; import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.ssl.SslFilter; +import org.junit.Assert; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,18 +44,24 @@ import quickfix.mina.ProtocolFactory; import quickfix.mina.SessionConnector; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; -import javax.security.cert.X509Certificate; import java.lang.reflect.Field; import java.math.BigInteger; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import org.apache.mina.util.AvailablePortFinder; import org.junit.After; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + public class SSLCertificateTest { // Note: To diagnose cipher suite errors, run with -Djavax.net.debug=ssl:handshake @@ -64,6 +72,7 @@ public void cleanup() { try { Thread.sleep(500); } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); java.util.logging.Logger.getLogger(SSLCertificateTest.class.getName()).log(Level.SEVERE, null, ex); } } @@ -100,6 +109,111 @@ public void shouldAuthenticateServerCertificate() throws Exception { } } + /** + * Server certificate has Common Name = localhost and no Server Alternative Name extension. + */ + @Test + public void shouldAuthenticateServerNameUsingServerCommonName() throws Exception { + int freePort = AvailablePortFinder.getNextAvailable(); + TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server-cn.keystore", false, + "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + + try { + acceptor.start(); + + TestInitiator initiator = new TestInitiator( + createInitiatorSettings("single-session/empty.keystore", "single-session/client-cn.truststore", + CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); + + try { + initiator.start(); + + initiator.assertNoSslExceptionThrown(); + initiator.assertLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ZULU", "ALFA")); + initiator.assertAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ZULU", "ALFA"), + new BigInteger("1683903911")); + + acceptor.assertNoSslExceptionThrown(); + acceptor.assertLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); + acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); + } finally { + initiator.stop(); + } + } finally { + acceptor.stop(); + } + } + + /** + * Server certificate has Common Name = server, but it has Server Alternative Name extension (DNS name). + */ + @Test + public void shouldAuthenticateServerNameUsingSNIExtension() throws Exception { + int freePort = AvailablePortFinder.getNextAvailable(); + TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server-sni.keystore", false, + "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + + try { + acceptor.start(); + + TestInitiator initiator = new TestInitiator( + createInitiatorSettings("single-session/empty.keystore", "single-session/client-sni.truststore", + CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); + + try { + initiator.start(); + + initiator.assertNoSslExceptionThrown(); + initiator.assertLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ZULU", "ALFA")); + initiator.assertAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ZULU", "ALFA"), + new BigInteger("1683904647")); + + acceptor.assertNoSslExceptionThrown(); + acceptor.assertLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); + acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); + } finally { + initiator.stop(); + } + } finally { + acceptor.stop(); + } + } + + /** + * Server certificate has Common Name = server and no Server Alternative Name extension. + */ + @Test + public void shouldFailWhenHostnameDoesNotMatchServerName() throws Exception { + int freePort = AvailablePortFinder.getNextAvailable(); + + TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server-bad-cn.keystore", false, + "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + + try { + acceptor.start(); + + TestInitiator initiator = new TestInitiator( + createInitiatorSettings("single-session/empty.keystore", "single-session/client-bad-cn.truststore", + CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS", "HTTPS")); + + try { + initiator.start(); + + initiator.assertSslExceptionThrown("No name matching localhost found", SSLHandshakeException.class); + initiator.assertNotLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ZULU", "ALFA")); + initiator.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ZULU", "ALFA")); + + acceptor.assertSslExceptionThrown("Received fatal alert: certificate_unknown", SSLHandshakeException.class); + acceptor.assertNotLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); + acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); + } finally { + initiator.stop(); + } + } finally { + acceptor.stop(); + } + } + @Test public void shouldAuthenticateServerAndClientCertificates() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); @@ -444,19 +558,21 @@ static abstract class TestConnector { private final SessionConnector connector; private final CountDownLatch exceptionThrownLatch; + private final AtomicReference exception; public TestConnector(SessionSettings sessionSettings) throws ConfigError { this.connector = prepareConnector(sessionSettings); this.exceptionThrownLatch = new CountDownLatch(1); + this.exception = new AtomicReference<>(); } private SessionConnector prepareConnector(SessionSettings sessionSettings) throws ConfigError { SessionConnector sessionConnector = createConnector(sessionSettings); sessionConnector.setIoFilterChainBuilder(chain -> chain.addFirst("Exception handler", new IoFilterAdapter() { @Override - public void exceptionCaught(NextFilter nextFilter, IoSession session, Throwable cause) - throws Exception { + public void exceptionCaught(NextFilter nextFilter, IoSession session, Throwable cause) { LOGGER.info("exceptionCaught", cause); + exception.set(cause); exceptionThrownLatch.countDown(); nextFilter.exceptionCaught(session, cause); } @@ -474,12 +590,12 @@ private SSLSession findSSLSession(Session session) throws Exception { return null; IoFilterChain filterChain = ioSession.getFilterChain(); - SSLFilter sslFilter = (SSLFilter) filterChain.get(SSLSupport.FILTER_NAME); + SslFilter sslFilter = (SslFilter) filterChain.get(SSLSupport.FILTER_NAME); if (sslFilter == null) return null; - return sslFilter.getSslSession(ioSession); + return (SSLSession) ioSession.getAttribute(SslFilter.SSL_SECURED); } private Session findSession(SessionID sessionID) { @@ -507,10 +623,14 @@ public void assertAuthenticated(SessionID sessionID, BigInteger serialNumber) th Session session = findSession(sessionID); SSLSession sslSession = findSSLSession(session); - X509Certificate[] peerCertificateChain = sslSession.getPeerCertificateChain(); + Certificate[] peerCertificates = sslSession.getPeerCertificates(); + + for (Certificate peerCertificate : peerCertificates) { + if (!(peerCertificate instanceof X509Certificate)) { + continue; + } - for (X509Certificate certificate : peerCertificateChain) { - if (certificate.getSerialNumber().compareTo(serialNumber) == 0) { + if (((X509Certificate)peerCertificate).getSerialNumber().compareTo(serialNumber) == 0) { return; } } @@ -526,23 +646,23 @@ public void assertNotAuthenticated(SessionID sessionID) throws Exception { return; try { - X509Certificate[] peerCertificateChain = sslSession.getPeerCertificateChain(); + Certificate[] peerCertificates = sslSession.getPeerCertificates(); - if (peerCertificateChain != null && peerCertificateChain.length > 0) { + if (peerCertificates != null && peerCertificates.length > 0) { throw new AssertionError("Certificate was authenticated"); } } catch (SSLPeerUnverifiedException e) { } } - public void assertLoggedOn(SessionID sessionID) throws InterruptedException { + public void assertLoggedOn(SessionID sessionID) { Session session = findSession(sessionID); if (!session.isLoggedOn()) throw new AssertionError("Session is not logged on"); } - public void assertNotLoggedOn(SessionID sessionID) throws InterruptedException { + public void assertNotLoggedOn(SessionID sessionID) { Session session = findSession(sessionID); if (session.isLoggedOn()) @@ -550,11 +670,25 @@ public void assertNotLoggedOn(SessionID sessionID) throws InterruptedException { } public void assertSslExceptionThrown() throws Exception { + assertSslExceptionThrown(null, null); + } + + public void assertSslExceptionThrown(String errorMessage, Class errorType) throws Exception { boolean reachedZero = exceptionThrownLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); if (!reachedZero) { throw new AssertionError("No SSL exception thrown"); } + + Throwable throwable = exception.get(); + + if (errorMessage != null) { + assertEquals(errorMessage, throwable.getMessage()); + } + + if (errorType != null) { + assertSame(errorType, throwable.getClass()); + } } public void assertNoSslExceptionThrown() throws Exception { @@ -705,8 +839,14 @@ private SessionSettings createAcceptorSettings(String keyStoreName, boolean need } private SessionSettings createInitiatorSettings(String keyStoreName, String trustStoreName, String cipherSuites, - String protocols, String senderId, String targetId, String port, String keyStoreType, - String trustStoreType) { + String protocols, String senderId, String targetId, String port, String keyStoreType, + String trustStoreType) { + return createInitiatorSettings(keyStoreName, trustStoreName, cipherSuites, protocols, senderId, targetId, port,keyStoreType, trustStoreType, null); + } + + private SessionSettings createInitiatorSettings(String keyStoreName, String trustStoreName, String cipherSuites, + String protocols, String senderId, String targetId, String port, String keyStoreType, + String trustStoreType, String endpointIdentificationAlgorithm) { HashMap defaults = new HashMap<>(); defaults.put("ConnectionType", "initiator"); defaults.put("SocketConnectProtocol", ProtocolFactory.getTypeString(ProtocolFactory.SOCKET)); @@ -742,6 +882,10 @@ private SessionSettings createInitiatorSettings(String keyStoreName, String trus defaults.put(SSLSupport.SETTING_ENABLED_PROTOCOLS, protocols); } + if (endpointIdentificationAlgorithm != null) { + defaults.put(SSLSupport.SETTING_ENDPOINT_IDENTIFICATION_ALGORITHM, endpointIdentificationAlgorithm); + } + SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, senderId, targetId); SessionSettings sessionSettings = new SessionSettings(); diff --git a/quickfixj-core/src/test/java/quickfix/mina/ssl/X509TrustManagerWrapperTest.java b/quickfixj-core/src/test/java/quickfix/mina/ssl/X509TrustManagerWrapperTest.java index 95fa131945..b2d45214c6 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/ssl/X509TrustManagerWrapperTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/X509TrustManagerWrapperTest.java @@ -11,6 +11,9 @@ import org.junit.Test; import org.mockito.Mockito; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + public class X509TrustManagerWrapperTest { private X509TrustManagerWrapper underTest; @@ -18,14 +21,14 @@ public class X509TrustManagerWrapperTest { @Before public void setUp() throws Exception { - trustManager = Mockito.mock(X509TrustManager.class); + trustManager = mock(X509TrustManager.class); underTest = new X509TrustManagerWrapper(trustManager); } @Test public void shouldWrapX509TrustManagers() { - TrustManager[] managers = new TrustManager[] { Mockito.mock(TrustManager.class), - Mockito.mock(X509TrustManager.class) }; + TrustManager[] managers = new TrustManager[]{mock(TrustManager.class), + mock(X509TrustManager.class)}; TrustManager[] wrapped = X509TrustManagerWrapper.wrap(managers); @@ -38,8 +41,8 @@ public void shouldWrapX509TrustManagers() { @Test(expected = CertificateException.class) public void shouldRethrowCertificateExceptionOnCheckClientTrusted() throws Exception { // underlying trust manager should throw runtime exception - Mockito.doThrow(new RuntimeException()).when(trustManager) - .checkClientTrusted(Mockito.any(X509Certificate[].class), Mockito.anyString()); + doThrow(new RuntimeException()).when(trustManager) + .checkClientTrusted(Mockito.any(), Mockito.any()); underTest.checkClientTrusted(null, null); } @@ -47,8 +50,8 @@ public void shouldRethrowCertificateExceptionOnCheckClientTrusted() throws Excep @Test(expected = CertificateException.class) public void shouldRethrowCertificateExceptionOnCheckServerTrusted() throws Exception { // underlying trust manager should throw runtime exception - Mockito.doThrow(new RuntimeException()).when(trustManager) - .checkServerTrusted(Mockito.any(X509Certificate[].class), Mockito.anyString()); + doThrow(new RuntimeException()).when(trustManager) + .checkServerTrusted(Mockito.any(), Mockito.any()); underTest.checkServerTrusted(null, null); } diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/ATApplication.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/ATApplication.java index 97515579e3..18cf248ea4 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/ATApplication.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/ATApplication.java @@ -19,8 +19,7 @@ package quickfix.test.acceptance; -import java.io.IOException; - +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Assert; import quickfix.Application; @@ -38,21 +37,19 @@ public class ATApplication implements Application { private final ATMessageCracker inboundCracker = new ATMessageCracker(); private final MessageCracker outboundCracker = new MessageCracker(new Object()); - private boolean isLoggedOn; + private final AtomicBoolean isLoggedOn = new AtomicBoolean(false); + @Override public void onCreate(SessionID sessionID) { - try { - assertNoSessionLock(sessionID); - Session.lookupSession(sessionID).reset(); - } catch (IOException e) { - e.printStackTrace(); - } + assertNoSessionLock(sessionID); + Session.lookupSession(sessionID).reset(); } - public synchronized void onLogon(SessionID sessionID) { + @Override + public void onLogon(SessionID sessionID) { assertNoSessionLock(sessionID); - Assert.assertFalse("Already logged on", isLoggedOn); - isLoggedOn = true; + Assert.assertFalse("Already logged on", isLoggedOn.get()); + isLoggedOn.set(true); } private void assertNoSessionLock(SessionID sessionID) { @@ -62,17 +59,20 @@ private void assertNoSessionLock(SessionID sessionID) { Thread.holdsLock(session)); } - public synchronized void onLogout(SessionID sessionID) { + @Override + public void onLogout(SessionID sessionID) { assertNoSessionLock(sessionID); inboundCracker.reset(); - Assert.assertTrue("No logged on when logout is received", isLoggedOn); - isLoggedOn = false; + Assert.assertTrue("Not logged on when logout is received", isLoggedOn.get()); + isLoggedOn.set(false); } + @Override public void toAdmin(Message message, SessionID sessionID) { assertNoSessionLock(sessionID); } + @Override public void toApp(Message message, SessionID sessionID) throws DoNotSend { assertNoSessionLock(sessionID); try { @@ -84,11 +84,13 @@ public void toApp(Message message, SessionID sessionID) throws DoNotSend { } } + @Override public void fromAdmin(Message message, SessionID sessionID) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { assertNoSessionLock(sessionID); } + @Override public void fromApp(Message message, SessionID sessionID) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { assertNoSessionLock(sessionID); diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/ATMessageCracker.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/ATMessageCracker.java index 47afdb6cd4..08f7744b97 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/ATMessageCracker.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/ATMessageCracker.java @@ -75,6 +75,11 @@ public void onMessage(quickfix.fix50sp2.NewOrderSingle message, SessionID sessio process(message, sessionID); } + public void onMessage(quickfix.fixlatest.NewOrderSingle message, SessionID sessionID) + throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { + process(message, sessionID); + } + public void onMessage(quickfix.fix50.SecurityDefinition message, SessionID sessionID) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { try { @@ -84,6 +89,15 @@ public void onMessage(quickfix.fix50.SecurityDefinition message, SessionID sessi } } + public void onMessage(quickfix.fixlatest.SecurityDefinition message, SessionID sessionID) + throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { + try { + Session.sendToTarget(message, sessionID); + } catch (SessionNotFound snf) { + snf.printStackTrace(); + } + } + public void onMessage(quickfix.fix44.NewOrderSingle message, SessionID sessionID) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { process(message, sessionID); diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/ATServer.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/ATServer.java index cbed9a2bac..fd6bb5d62b 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/ATServer.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/ATServer.java @@ -29,7 +29,6 @@ import quickfix.FixVersions; import quickfix.MemoryStoreFactory; import quickfix.MessageStoreFactory; -import quickfix.RuntimeError; import quickfix.SLF4JLogFactory; import quickfix.SessionID; import quickfix.SessionSettings; @@ -38,12 +37,11 @@ import quickfix.mina.ProtocolFactory; import quickfix.mina.acceptor.AbstractSocketAcceptor; import quickfix.mina.ssl.SSLSupport; -import quickfix.test.util.ReflectionUtil; +import quickfix.test.util.StackTraceUtil; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; -import java.net.BindException; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; @@ -53,6 +51,8 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class ATServer implements Runnable { private final Logger log = LoggerFactory.getLogger(ATServer.class); @@ -72,6 +72,9 @@ public class ATServer implements Runnable { private String keyStorePassword; private Map overridenProperties = null; + //Pattern to get FIX version from test location example :"fixLatest/20_SimultaneousResendRequest.def" + protected static final Pattern fixVersionFromTestLocationPattern = Pattern.compile("^(.*?)(?:[\\/,\\\\].*)$"); + public ATServer() { // defaults } @@ -90,9 +93,13 @@ public ATServer(TestSuite suite, boolean threaded, int transportType, int port, this.overridenProperties = overridenProperties; this.transportType = transportType; this.port = port; + // determine the FIX versions, by convention the first part of the name (location) of the test. Enumeration e = suite.tests(); while (e.hasMoreElements()) { - fixVersions.add(e.nextElement().toString().substring(0, 5)); + Matcher matcher = fixVersionFromTestLocationPattern.matcher(e.nextElement().toString()); + if (matcher.find()) { + fixVersions.add(matcher.group(1)); + } } resetOnDisconnect = true; log.info("creating sessions for {}", fixVersions); @@ -107,6 +114,7 @@ public void run() { defaults.put("SocketTcpNoDelay", "Y"); defaults.put("StartTime", "00:00:00"); defaults.put("EndTime", "00:00:00"); + defaults.put("NonStopSession", "Y"); defaults.put("SenderCompID", "ISLD"); defaults.put("TargetCompID", "TW"); defaults.put("JdbcDriver", "com.mysql.jdbc.Driver"); @@ -158,6 +166,10 @@ public void run() { acceptFixVersion(FixVersions.BEGINSTRING_FIXT11); } + if (fixVersions.contains("fixLatest")) { + acceptFixVersion(FixVersions.BEGINSTRING_FIXT11); + } + ATApplication application = new ATApplication(); MessageStoreFactory factory = usingMemoryStore ? new MemoryStoreFactory() @@ -176,26 +188,19 @@ public void run() { assertSessionIds(); acceptor.setIoFilterChainBuilder(ioFilterChainBuilder); - try { - acceptor.start(); - } catch (RuntimeError e) { - if (e.getCause() instanceof BindException) { - log.warn("Acceptor port {} is still bound! Waiting 60 seconds and trying again...", port); - Thread.sleep(60000); - acceptor.start(); - } - } + acceptor.start(); assertSessionIds(); initializationLatch.countDown(); + CountDownLatch shutdownLatch = new CountDownLatch(1); try { // running all acceptance tests should hopefully not take longer than 30 mins final boolean await = shutdownLatch.await(30, TimeUnit.MINUTES); if (!await) { log.error("ShutdownLatch timed out. Dumping threads..."); - ReflectionUtil.dumpStackTraces(); + StackTraceUtil.dumpStackTraces(log); final ThreadMXBean bean = ManagementFactory.getThreadMXBean(); long[] threadIds = bean.findDeadlockedThreads(); diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/AcceptanceTestSuite.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/AcceptanceTestSuite.java index 72317fb7bd..deacc7c4ab 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/AcceptanceTestSuite.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/AcceptanceTestSuite.java @@ -7,9 +7,6 @@ import junit.framework.TestResult; import junit.framework.TestSuite; import org.apache.mina.util.AvailablePortFinder; -import org.logicalcobwebs.proxool.ProxoolException; -import org.logicalcobwebs.proxool.ProxoolFacade; -import org.logicalcobwebs.proxool.admin.SnapshotIF; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import quickfix.Session; @@ -38,7 +35,7 @@ public class AcceptanceTestSuite extends TestSuite { private static final String acceptanceTestBaseDir = AcceptanceTestSuite.class.getClassLoader().getResource(acceptanceTestResourcePath).getPath(); private static int transportType = ProtocolFactory.SOCKET; - private static int port = 9887; + private static int port = AvailablePortFinder.getNextAvailable(); private final boolean skipSlowTests; private final boolean multithreaded; @@ -73,6 +70,7 @@ public void run(TestResult result) { TestConnection connection = null; String failureString = "test " + filename + " failed with message: "; try { + log.info("Running test {}, filename : {}", this.testname, this.filename); connection = new TestConnection(); List testSteps = load(filename); for (TestStep testStep : testSteps) { @@ -93,26 +91,10 @@ public void run(TestResult result) { //printDatabasePoolingStatistics(); } - @SuppressWarnings("unused") - protected void printDatabasePoolingStatistics() { - try { - for (String alias : ProxoolFacade.getAliases()) { - SnapshotIF snapshot = ProxoolFacade.getSnapshot(alias, true); - System.out.println("active:" + snapshot.getActiveConnectionCount() + ",max:" - + snapshot.getMaximumConnectionCount() + ",served:" - + snapshot.getServedCount()); - } - } catch (ProxoolException e) { - e.printStackTrace(); - } - } - private List load(String filename) throws IOException { ArrayList steps = new ArrayList<>(); log.info("load test: " + filename); - BufferedReader in = null; - try { - in = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "ISO8859_1")); + try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(filename), "ISO8859_1"))) { String line = in.readLine(); while (line != null) { if (line.matches("^[ \t]*#.*")) { @@ -130,14 +112,6 @@ private List load(String filename) throws IOException { } line = in.readLine(); } - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e1) { - e1.printStackTrace(); - } - } } return steps; } @@ -171,6 +145,7 @@ public AcceptanceTestSuite(String testDirectory, boolean multithreaded, Map overridenProperties; -// private Thread serverThread; private final ExecutorService executor = Executors.newSingleThreadExecutor(); private ATServer server; @@ -247,7 +221,6 @@ protected void tearDown() throws Exception { public static Test suite() { transportType = ProtocolFactory.getTransportType(System.getProperty(ATEST_TRANSPORT_KEY, ProtocolFactory.getTypeString(ProtocolFactory.SOCKET))); - port = AvailablePortFinder.getNextAvailable(port); TestSuite acceptanceTests = new TestSuite(AcceptanceTestSuite.class.getSimpleName()); // default server acceptanceTests.addTest(new AcceptanceTestServerSetUp(new AcceptanceTestSuite("server", false))); diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/ConnectToServerStep.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/ConnectToServerStep.java index f64e7a084d..e92888f2bc 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/ConnectToServerStep.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/ConnectToServerStep.java @@ -61,7 +61,7 @@ public void run(TestResult result, TestConnection connection) { try { Thread.sleep(reconnectDelay); } catch (InterruptedException e1) { - e1.printStackTrace(); + Thread.currentThread().interrupt(); } } try { diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/ExpectMessageStep.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/ExpectMessageStep.java index ba4e3ac33e..121aa5248c 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/ExpectMessageStep.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/ExpectMessageStep.java @@ -23,7 +23,7 @@ import org.junit.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import quickfix.test.util.ReflectionUtil; +import quickfix.test.util.StackTraceUtil; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; @@ -77,7 +77,7 @@ public void run(TestResult result, final TestConnection connection) throws Inter CharSequence message = connection.readMessage(clientId, TIMEOUT_IN_MS); if (message == null) { log.info("Dumping threads due to timeout when expecting a message..."); - ReflectionUtil.dumpStackTraces(); + StackTraceUtil.dumpStackTraces(log); final ThreadMXBean bean = ManagementFactory.getThreadMXBean(); long[] threadIds = bean.findDeadlockedThreads(); diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/InitiateMessageStep.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/InitiateMessageStep.java index 030b3e5e74..65f07e63cd 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/InitiateMessageStep.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/InitiateMessageStep.java @@ -99,9 +99,10 @@ public void run(TestResult result, TestConnection connection) { log.debug("sending to client " + clientId + ": " + message); try { connection.sendMessage(clientId, message); - } catch (IOException e) { + } catch (Exception e) { AssertionFailedError error = new AssertionFailedError(message); error.setStackTrace(e.getStackTrace()); + log.error("Exception in InitiateMessageStep", e); throw error; } } diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/RegularExpressionTest.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/RegularExpressionTest.java new file mode 100644 index 0000000000..267bd2388d --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/RegularExpressionTest.java @@ -0,0 +1,25 @@ +package quickfix.test.acceptance; + +import org.junit.Test; + +import java.util.regex.Matcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class RegularExpressionTest { + + private static final String fixVersion = "fixLatest"; + private static final String unixPathString = fixVersion + "/20_SimultaneousResendRequest.def"; + private static final String windowsPathString = fixVersion + "\\20_SimultaneousResendRequest.def"; + @Test + public void testRegularExpressionForExtractingFixVersionFromPath() { + Matcher matcher = ATServer.fixVersionFromTestLocationPattern.matcher(unixPathString); + assertTrue (matcher.find()); + assertEquals(fixVersion, matcher.group(1)); + + matcher = ATServer.fixVersionFromTestLocationPattern.matcher(windowsPathString); + assertTrue (matcher.find()); + assertEquals(fixVersion, matcher.group(1)); + } +} diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/TestConnection.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/TestConnection.java index 36fc4f0403..14087e278a 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/TestConnection.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/TestConnection.java @@ -33,7 +33,7 @@ import org.slf4j.LoggerFactory; import quickfix.mina.ProtocolFactory; import quickfix.mina.message.FIXProtocolCodecFactory; -import quickfix.test.util.ReflectionUtil; +import quickfix.test.util.StackTraceUtil; import java.io.IOException; import java.lang.management.ManagementFactory; @@ -44,16 +44,19 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import quickfix.mina.SessionConnector; public class TestConnection { - private static final HashMap connectors = new HashMap<>(); + private static final Map connectors = new HashMap<>(); + private final ConcurrentMap ioHandlers = new ConcurrentHashMap<>(); private final Logger log = LoggerFactory.getLogger(getClass()); - private final HashMap ioHandlers = new HashMap<>(); public void sendMessage(int clientId, String message) throws IOException { TestIoHandler handler = getIoHandler(clientId); @@ -61,15 +64,16 @@ public void sendMessage(int clientId, String message) throws IOException { } private TestIoHandler getIoHandler(int clientId) { - synchronized (ioHandlers) { - return ioHandlers.get(clientId); - } + return ioHandlers.get(clientId); } public void tearDown() { for (TestIoHandler testIoHandler : ioHandlers.values()) { - CloseFuture closeFuture = testIoHandler.getSession().closeNow(); - closeFuture.awaitUninterruptibly(); + IoSession session = testIoHandler.getSession(); + if (session != null) { + CloseFuture closeFuture = session.closeNow(); + closeFuture.awaitUninterruptibly(); + } } ioHandlers.clear(); } @@ -84,7 +88,7 @@ public void waitForClientDisconnect(int clientId) throws IOException, Interrupte public void connect(int clientId, int transportType, int port) throws IOException { - IoConnector connector = connectors.get(Integer.toString(clientId)); + IoConnector connector = connectors.get(clientId); if (connector != null) { SessionConnector.closeManagedSessionsAndDispose(connector, true, log); } @@ -99,16 +103,16 @@ public void connect(int clientId, int transportType, int port) } else { throw new RuntimeException("Unsupported transport type: " + transportType); } - connectors.put(Integer.toString(clientId), connector); + connectors.put(clientId, connector); TestIoHandler testIoHandler = new TestIoHandler(); - synchronized (ioHandlers) { - ioHandlers.put(clientId, testIoHandler); - connector.setHandler(testIoHandler); - ConnectFuture future = connector.connect(address); - future.awaitUninterruptibly(5000L); - Assert.assertTrue("connection to server failed", future.isConnected()); - } + ioHandlers.put(clientId, testIoHandler); + connector.setHandler(testIoHandler); + ConnectFuture future = connector.connect(address); + future.awaitUninterruptibly(5000L); + Throwable exception = future.getException(); + String failedMessage = "connection to server failed: " + (exception != null ? exception.getMessage() : ""); + Assert.assertTrue(failedMessage, future.isConnected()); } private class TestIoHandler extends IoHandlerAdapter { @@ -141,10 +145,10 @@ public void messageReceived(IoSession session, Object message) throws Exception public IoSession getSession() { try { - boolean await = sessionCreatedLatch.await(70, TimeUnit.SECONDS); // 10 seconds more than retry time in ATServer.run() + boolean await = sessionCreatedLatch.await(5, TimeUnit.SECONDS); if (!await) { log.error("sessionCreatedLatch timed out. Dumping threads..."); - ReflectionUtil.dumpStackTraces(); + StackTraceUtil.dumpStackTraces(log); final ThreadMXBean bean = ManagementFactory.getThreadMXBean(); long[] threadIds = bean.findDeadlockedThreads(); @@ -165,6 +169,7 @@ public IoSession getSession() { } } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); throw new RuntimeException(e); } return session; @@ -175,7 +180,10 @@ public String getNextMessage(long timeout) throws InterruptedException { } public void waitForDisconnect() throws InterruptedException { - if (!disconnectLatch.await(500000L, TimeUnit.MILLISECONDS)) { + /* Please note that this timeout should not be too little because + there is at least one acceptance test (6_SendTestRequest) expecting + the connection to timeout after a TestRequest. */ + if (!disconnectLatch.await(20000, TimeUnit.MILLISECONDS)) { Assert.fail("client not disconnected"); } } diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTest.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTest.java index f7a1487f9f..60ffbbb7e1 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTest.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTest.java @@ -19,6 +19,7 @@ package quickfix.test.acceptance.resynch; +import org.apache.mina.util.AvailablePortFinder; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -33,11 +34,13 @@ public class ResynchTest { ResynchTestServer server; + int port; @Before public void setUp() throws Exception { SystemTime.setTimeSource(null); - server = new ResynchTestServer(); + port = AvailablePortFinder.getNextAvailable(); + server = new ResynchTestServer(port); } @After @@ -49,7 +52,7 @@ public void tearDown() throws Exception { public void testAcceptorTimerSync() throws ConfigError, SessionNotFound, InterruptedException { server.start(); server.waitForInitialization(); - new ResynchTestClient().run(); + new ResynchTestClient(port).run(); } @Test(timeout=30000) @@ -58,7 +61,7 @@ public void testAcceptorTimerUnsyncWithValidatingSequenceNumbers() throws Config server.setValidateSequenceNumbers(true); server.start(); server.waitForInitialization(); - ResynchTestClient client = new ResynchTestClient(); + ResynchTestClient client = new ResynchTestClient(port); client.setUnsynchMode(true); client.run(); } @@ -69,7 +72,7 @@ public void testAcceptorTimerUnsyncWithoutValidatingSequenceNumbers() throws Con server.setValidateSequenceNumbers(false); server.start(); server.waitForInitialization(); - ResynchTestClient client = new ResynchTestClient(); + ResynchTestClient client = new ResynchTestClient(port); client.setUnsynchMode(false); client.setForceResynch(true); client.run(); diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTestClient.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTestClient.java index d618d6206d..122aa37b27 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTestClient.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTestClient.java @@ -55,10 +55,17 @@ public class ResynchTestClient extends MessageCracker implements Application { private final SessionSettings settings = new SessionSettings(); private final CountDownLatch shutdownLatch = new CountDownLatch(1); private boolean failed; + private final int port; private boolean unsynchMode = false; private boolean forceResynch = false; + public ResynchTestClient(int port) { + this.port = port; + } + + + public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { try { @@ -102,7 +109,7 @@ public void run() throws ConfigError, SessionNotFound, InterruptedException { defaults.put("ConnectionType", "initiator"); defaults.put("HeartBtInt", "2"); defaults.put("SocketConnectHost", "localhost"); - defaults.put("SocketConnectPort", "19889"); + defaults.put("SocketConnectPort", String.valueOf(port)); defaults.put("SocketTcpNoDelay", "Y"); defaults.put("ReconnectInterval", "3"); defaults.put("StartTime", "00:00:00"); @@ -154,12 +161,6 @@ public void toAdmin(Message message, SessionID sessionId) { public void toApp(Message message, SessionID sessionId) throws DoNotSend { } - public static void main(String[] args) throws ConfigError, SessionNotFound, - InterruptedException { - ResynchTestClient ttc = new ResynchTestClient(); - ttc.run(); - } - public void setUnsynchMode(boolean unsynchMode) { this.unsynchMode = unsynchMode; } diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTestServer.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTestServer.java index 0b714b409d..cabb02fb3d 100644 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTestServer.java +++ b/quickfixj-core/src/test/java/quickfix/test/acceptance/resynch/ResynchTestServer.java @@ -55,10 +55,15 @@ public class ResynchTestServer extends MessageCracker implements Application, Ru private Thread serverThread; private final CountDownLatch initializationLatch = new CountDownLatch(1); private final CountDownLatch shutdownLatch = new CountDownLatch(1); + private final int port; private boolean unsynchMode = false; private boolean validateSequenceNumbers = true; + public ResynchTestServer(int port) { + this.port = port; + } + @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { @@ -107,7 +112,7 @@ public void run() { try { HashMap defaults = new HashMap<>(); defaults.put("ConnectionType", "acceptor"); - defaults.put("SocketAcceptPort", "19889"); + defaults.put("SocketAcceptPort", String.valueOf(port)); defaults.put("StartTime", "00:00:00"); defaults.put("EndTime", "00:00:00"); defaults.put("SenderCompID", "ISLD"); @@ -161,11 +166,6 @@ public void waitForInitialization() throws InterruptedException { initializationLatch.await(); } - public static void main(String[] args) { - ResynchTestServer server = new ResynchTestServer(); - server.run(); - } - public void setUnsynchMode(boolean unsynchMode) { this.unsynchMode = unsynchMode; } diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/timer/TimerTestClient.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/timer/TimerTestClient.java deleted file mode 100644 index 55765bcd83..0000000000 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/timer/TimerTestClient.java +++ /dev/null @@ -1,150 +0,0 @@ -/******************************************************************************* - * Copyright (c) quickfixengine.org All rights reserved. - * - * This file is part of the QuickFIX FIX Engine - * - * This file may be distributed under the terms of the quickfixengine.org - * license as defined by quickfixengine.org and appearing in the file - * LICENSE included in the packaging of this file. - * - * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING - * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE. - * - * See http://www.quickfixengine.org/LICENSE for licensing information. - * - * Contact ask@quickfixengine.org if any conditions of this licensing - * are not clear to you. - ******************************************************************************/ - -package quickfix.test.acceptance.timer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import quickfix.Application; -import quickfix.ConfigError; -import quickfix.DefaultMessageFactory; -import quickfix.DoNotSend; -import quickfix.FieldNotFound; -import quickfix.FixVersions; -import quickfix.IncorrectDataFormat; -import quickfix.IncorrectTagValue; -import quickfix.Initiator; -import quickfix.MemoryStoreFactory; -import quickfix.Message; -import quickfix.MessageCracker; -import quickfix.MessageStoreFactory; -import quickfix.RejectLogon; -import quickfix.RuntimeError; -import quickfix.SLF4JLogFactory; -import quickfix.SessionID; -import quickfix.SessionNotFound; -import quickfix.SessionSettings; -import quickfix.SocketInitiator; -import quickfix.UnsupportedMessageType; -import quickfix.fix44.ListStatusRequest; -import quickfix.fix44.TestRequest; - -import java.util.HashMap; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.CountDownLatch; - -/** - * @author John Hensley - */ -public class TimerTestClient extends MessageCracker implements Application { - private final Logger log = LoggerFactory.getLogger(TimerTestServer.class); - private final SessionSettings settings = new SessionSettings(); - private final CountDownLatch shutdownLatch = new CountDownLatch(1); - private boolean failed; - - public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, - IncorrectDataFormat, IncorrectTagValue, RejectLogon { - } - - public void fromApp(Message message, SessionID sessionID) throws FieldNotFound, - IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { - crack(message, sessionID); - } - - public void onCreate(SessionID sessionId) { - } - - public void onLogon(SessionID sessionId) { - } - - public void onLogout(SessionID sessionId) { - } - - public void onMessage(ListStatusRequest message, SessionID sessionID) { - log.info("got ListStatusRequest"); - } - - private void stop(boolean failed) { - this.failed = failed; - shutdownLatch.countDown(); - } - - public void run() throws ConfigError, SessionNotFound, InterruptedException { - HashMap defaults = new HashMap<>(); - defaults.put("ConnectionType", "initiator"); - defaults.put("HeartBtInt", "2"); - defaults.put("SocketConnectHost", "localhost"); - defaults.put("SocketConnectPort", "19888"); - defaults.put("SocketTcpNoDelay", "Y"); - defaults.put("StartTime", "00:00:00"); - defaults.put("EndTime", "00:00:00"); - defaults.put("SenderCompID", "TW"); - defaults.put("TargetCompID", "ISLD"); - defaults.put("FileStorePath", "target/data/timer_test"); - defaults.put("ValidateUserDefinedFields", "Y"); - settings.set(defaults); - - SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "TW", "ISLD"); - settings.setString(sessionID, "BeginString", FixVersions.BEGINSTRING_FIX44); - settings.setString(sessionID, "DataDictionary", "FIX44.xml"); - - MessageStoreFactory storeFactory = new MemoryStoreFactory(); - Initiator initiator = new SocketInitiator(this, storeFactory, settings, - new SLF4JLogFactory(settings), new DefaultMessageFactory()); - initiator.start(); - - try { - Timer timer = new Timer(); - timer.schedule(new TimerTask() { - public void run() { - stop(false); - } - }, 5000); - - try { - shutdownLatch.await(); - } catch (InterruptedException e) { - } - - if (failed) { - String message = "TimerTestClient had to send a test request, indicating that the test server was not reliably sending heartbeats."; - log.error(message); - throw new RuntimeError(message); - } - } finally { - initiator.stop(); - } - } - - public void toAdmin(Message message, SessionID sessionId) { - if (message instanceof TestRequest) { - stop(true); - } - } - - public void toApp(Message message, SessionID sessionId) throws DoNotSend { - } - - public static void main(String[] args) throws ConfigError, SessionNotFound, - InterruptedException { - TimerTestClient ttc = new TimerTestClient(); - ttc.run(); - } -} diff --git a/quickfixj-core/src/test/java/quickfix/test/acceptance/timer/TimerTestServer.java b/quickfixj-core/src/test/java/quickfix/test/acceptance/timer/TimerTestServer.java deleted file mode 100644 index f6ca060b9b..0000000000 --- a/quickfixj-core/src/test/java/quickfix/test/acceptance/timer/TimerTestServer.java +++ /dev/null @@ -1,169 +0,0 @@ -/******************************************************************************* - * Copyright (c) quickfixengine.org All rights reserved. - * - * This file is part of the QuickFIX FIX Engine - * - * This file may be distributed under the terms of the quickfixengine.org - * license as defined by quickfixengine.org and appearing in the file - * LICENSE included in the packaging of this file. - * - * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING - * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE. - * - * See http://www.quickfixengine.org/LICENSE for licensing information. - * - * Contact ask@quickfixengine.org if any conditions of this licensing - * are not clear to you. - ******************************************************************************/ - -package quickfix.test.acceptance.timer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import quickfix.Application; -import quickfix.DefaultMessageFactory; -import quickfix.DoNotSend; -import quickfix.FieldNotFound; -import quickfix.FixVersions; -import quickfix.IncorrectDataFormat; -import quickfix.IncorrectTagValue; -import quickfix.MemoryStoreFactory; -import quickfix.Message; -import quickfix.MessageCracker; -import quickfix.MessageStoreFactory; -import quickfix.RejectLogon; -import quickfix.SLF4JLogFactory; -import quickfix.Session; -import quickfix.SessionID; -import quickfix.SessionNotFound; -import quickfix.SessionSettings; -import quickfix.SocketAcceptor; -import quickfix.UnsupportedMessageType; -import quickfix.field.ListID; -import quickfix.fix44.ListStatusRequest; -import quickfix.fix44.Logon; - -import java.util.HashMap; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.CountDownLatch; - -/** - * @author John Hensley - */ -public class TimerTestServer extends MessageCracker implements Application, Runnable { - SocketAcceptor acceptor; - private final Logger log = LoggerFactory.getLogger(TimerTestServer.class); - private final SessionSettings settings = new SessionSettings(); - private Thread serverThread; - private final CountDownLatch initializationLatch = new CountDownLatch(1); - private final CountDownLatch shutdownLatch = new CountDownLatch(1); - - private class DelayedTestRequest extends TimerTask { - final SessionID session; - - DelayedTestRequest(SessionID sessionID) { - this.session = sessionID; - } - - public void run() { - try { - log.info("Sending offset message"); - ListStatusRequest lsr = new ListStatusRequest(new ListID("somelist")); - Session.sendToTarget(lsr, this.session); - } catch (SessionNotFound sessionNotFound) { - // not going to happen - } - } - } - - public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, - IncorrectDataFormat, IncorrectTagValue, RejectLogon { - // sleep to move our timer off from the client's - if (message instanceof Logon) { - new Timer().schedule(new DelayedTestRequest(sessionId), 3000); - } - } - - public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, - IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { - } - - public void onCreate(SessionID sessionId) { - } - - public void onLogon(SessionID sessionId) { - } - - public void onLogout(SessionID sessionId) { - log.info("logout"); - shutdownLatch.countDown(); - } - - public void start() { - serverThread = new Thread(this, "TimerTestServer"); - serverThread.setDaemon(true); - serverThread.start(); - } - - public void stop() { - shutdownLatch.countDown(); - } - - public void run() { - try { - HashMap defaults = new HashMap<>(); - defaults.put("ConnectionType", "acceptor"); - defaults.put("SocketAcceptPort", "19888" ); - defaults.put("StartTime", "00:00:00"); - defaults.put("EndTime", "00:00:00"); - defaults.put("SenderCompID", "ISLD"); - defaults.put("TargetCompID", "TW"); - defaults.put("FileStorePath", "target/data/server"); - defaults.put("ValidateUserDefinedFields", "Y"); - defaults.put("ResetOnDisconnect", "Y"); - settings.set(defaults); - - SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, "ISLD", "TW"); - settings.setString(sessionID, "BeginString", FixVersions.BEGINSTRING_FIX44); - settings.setString(sessionID, "DataDictionary", FixVersions.BEGINSTRING_FIX44.replaceAll("\\.", "") + ".xml"); - - MessageStoreFactory factory = new MemoryStoreFactory(); - acceptor = new SocketAcceptor(this, factory, settings, new SLF4JLogFactory(settings), - new DefaultMessageFactory()); - acceptor.start(); - try { - //acceptor.waitForInitialization(); - initializationLatch.countDown(); - - try { - shutdownLatch.await(); - } catch (InterruptedException e) { - } - - log.info("TimerTestServer shutting down."); - } finally { - acceptor.stop(); - } - } catch (Throwable e) { - log.error("Error in TimerTestServer server: ", e); - initializationLatch.countDown(); - } - } - - public void toAdmin(Message message, SessionID sessionId) { - } - - public void toApp(Message message, SessionID sessionId) throws DoNotSend { - } - - public void waitForInitialization() throws InterruptedException { - initializationLatch.await(); - } - - public static void main(String[] args) { - TimerTestServer server = new TimerTestServer(); - server.run(); - } -} diff --git a/quickfixj-core/src/test/java/quickfix/test/util/ReflectionUtil.java b/quickfixj-core/src/test/java/quickfix/test/util/ReflectionUtil.java index e9c57276b5..55dc837504 100644 --- a/quickfixj-core/src/test/java/quickfix/test/util/ReflectionUtil.java +++ b/quickfixj-core/src/test/java/quickfix/test/util/ReflectionUtil.java @@ -1,9 +1,24 @@ +/******************************************************************************* + * 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.test.util; -import java.beans.BeanInfo; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -11,7 +26,7 @@ import java.util.Arrays; /** - * Helper class for using reflection. Initially it's focussed on + * Helper class for using reflection. Initially it's focused on * invoking methods, but other tools may be added in the future. */ public class ReflectionUtil { @@ -150,42 +165,4 @@ public static Object callStaticMethod(String methodFqn, Object[] args) return getMatchingMethod(targetClass, methodName, args).invoke(null, args); } - public static void dumpStackTraces() { - try { - Object threadMXBean = ReflectionUtil.callStaticMethod( - "java.lang.management.ManagementFactory.getThreadMXBean", null); - Class threadMXBeanInterface = Class.forName("java.lang.management.ThreadMXBean"); - long[] threadIds = (long[]) ReflectionUtil.callMethod(threadMXBean, - threadMXBeanInterface, "getAllThreadIds", null); - Object[] threadInfos = (Object[]) ReflectionUtil.callMethod(threadMXBean, - threadMXBeanInterface, "getThreadInfo", new Object[] { threadIds, 10 }); - for (Object threadInfo : threadInfos) { - System.out.println((String) ReflectionUtil.callMethod(threadInfo, - "getThreadName", null)); - BeanInfo info = Introspector.getBeanInfo(threadInfo.getClass()); - PropertyDescriptor[] parameters = info.getPropertyDescriptors(); - for (PropertyDescriptor parameter : parameters) { - if (parameter.getReadMethod() != null) { - Object value = parameter.getReadMethod().invoke(threadInfo, - (Object[]) null); - if (value != null && value.getClass().isArray()) { - System.out.println(" " + parameter.getName() + ":"); - for (int a = 0; a < Array.getLength(value); a++) { - System.out.println(" " + Array.get(value, a)); - } - } else { - if (value != null) { - System.out.println(" " + parameter.getName() + ": " + value); - } - } - } - } - System.out.println(); - } - } catch (Exception e) { - e.printStackTrace(); - // ignore, probably wrong JVM version - } - } - } diff --git a/quickfixj-core/src/test/java/quickfix/test/util/StackTraceUtil.java b/quickfixj-core/src/test/java/quickfix/test/util/StackTraceUtil.java new file mode 100644 index 0000000000..598a141151 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/test/util/StackTraceUtil.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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.test.util; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import org.slf4j.Logger; + +public class StackTraceUtil { + + private static final String DEADLOCKED_THREADS_STRING = "DEADLOCKED threads:" + System.lineSeparator(); + + private StackTraceUtil() { + } + + /** + * Prints stack traces for all threads via System.out. + */ + public static void dumpStackTraces() { + ThreadInfo[] threadInfos = getThreadInfos(); + printThreadInfo(threadInfos, null); + ThreadInfo[] deadlockedThreads = findDeadlockedThreads(null); + printThreadInfo(deadlockedThreads, null); + } + + /** + * Prints stack traces for all threads via passed Logger. + * + * @param log Logger instance to use + */ + public static void dumpStackTraces(Logger log) { + ThreadInfo[] threadInfos = getThreadInfos(); + printThreadInfo(threadInfos, log); + ThreadInfo[] deadlockedThreads = findDeadlockedThreads(log); + printThreadInfo(deadlockedThreads, log); + } + + private static void printThreadInfo(ThreadInfo[] threadInfos, Logger log) { + for (ThreadInfo threadInfo : threadInfos) { + if (log != null) { + log.error(threadInfo.toString()); + } else { + System.out.println(threadInfo.toString()); + } + } + } + + private static ThreadInfo[] getThreadInfos() { + ThreadMXBean bean = ManagementFactory.getThreadMXBean(); + long[] threadIds = bean.getAllThreadIds(); + ThreadInfo[] threadInfos = bean.getThreadInfo(threadIds, 15); + return threadInfos; + } + + private static ThreadInfo[] findDeadlockedThreads(Logger log) { + ThreadMXBean bean = ManagementFactory.getThreadMXBean(); + long[] deadlockedThreadsIDs = bean.findDeadlockedThreads(); + if (deadlockedThreadsIDs != null) { + if (log != null) { + log.error(DEADLOCKED_THREADS_STRING); + } else { + System.out.println(DEADLOCKED_THREADS_STRING); + } + return bean.getThreadInfo(deadlockedThreadsIDs); + } + return new ThreadInfo[]{}; + } + +} diff --git a/quickfixj-core/src/test/resources/FIX_External_DTD.xml b/quickfixj-core/src/test/resources/FIX_External_DTD.xml new file mode 100644 index 0000000000..b1953366fd --- /dev/null +++ b/quickfixj-core/src/test/resources/FIX_External_DTD.xml @@ -0,0 +1,27 @@ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + +
diff --git a/quickfixj-core/src/test/resources/configWithSessionVariables.ini b/quickfixj-core/src/test/resources/configWithSessionVariables.ini new file mode 100644 index 0000000000..e06f510dbc --- /dev/null +++ b/quickfixj-core/src/test/resources/configWithSessionVariables.ini @@ -0,0 +1,20 @@ +#comment +[DEFAULT] +Empty= +ConnectionType=acceptor +SocketAcceptPort=5001 +FileStorePath=store +StartTime=00:00:00 +EndTime=00:00:00 +TestLong=1234 +TestLong2=abcd +TestDouble=12.34 +TestDouble2=abcd +TestBoolTrue=Y +TestBoolFalse=N +SenderCompID=TW + +[SESSION] +BeginString=FIX.4.2 +TargetCompID=CLIENT3_${CLIENT_PLACEHOLDER1}_${CLIENT_PLACEHOLDER2} +DataDictionary=../spec/FIX42.xml diff --git a/quickfixj-core/src/test/resources/logging.properties b/quickfixj-core/src/test/resources/logging.properties index 059a6959f2..353d97cee2 100644 --- a/quickfixj-core/src/test/resources/logging.properties +++ b/quickfixj-core/src/test/resources/logging.properties @@ -1,5 +1,5 @@ handlers=java.util.logging.ConsoleHandler -java.util.logging.ConsoleHandler.level=WARN +java.util.logging.ConsoleHandler.level=ALL java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter -.level=WARN \ No newline at end of file +.level=ALL diff --git a/quickfixj-core/src/test/resources/quickfix/test/acceptance/definitions/lastMsgSeqNumProcessed/fixLatest/LastProcessedMsgSeqNum.def b/quickfixj-core/src/test/resources/quickfix/test/acceptance/definitions/lastMsgSeqNumProcessed/fixLatest/LastProcessedMsgSeqNum.def new file mode 100644 index 0000000000..dbf7963471 --- /dev/null +++ b/quickfixj-core/src/test/resources/quickfix/test/acceptance/definitions/lastMsgSeqNumProcessed/fixLatest/LastProcessedMsgSeqNum.def @@ -0,0 +1,21 @@ +iCONNECT + +I8=FIXT.1.135=A34=149=TW52=