├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── com │ └── qoomon │ └── banking │ ├── bic │ └── BIC.java │ ├── iban │ └── IBAN.java │ └── swift │ ├── bcsmessage │ ├── BCSMessage.java │ ├── BCSMessageParseException.java │ └── BCSMessageParser.java │ ├── message │ ├── SwiftMessage.java │ ├── SwiftMessageReader.java │ ├── block │ │ ├── ApplicationHeaderBlock.java │ │ ├── ApplicationHeaderInputBlock.java │ │ ├── ApplicationHeaderOutputBlock.java │ │ ├── BasicHeaderBlock.java │ │ ├── BlockUtils.java │ │ ├── GeneralBlock.java │ │ ├── SwiftBlock.java │ │ ├── SwiftBlockReader.java │ │ ├── SystemTrailerBlock.java │ │ ├── TextBlock.java │ │ ├── UserHeaderBlock.java │ │ ├── UserTrailerBlock.java │ │ └── exception │ │ │ ├── BlockFieldParseException.java │ │ │ └── BlockParseException.java │ └── exception │ │ └── SwiftMessageParseException.java │ ├── notation │ ├── FieldNotation.java │ ├── FieldNotationParseException.java │ ├── SwiftDecimalFormatter.java │ ├── SwiftNotation.java │ └── SwiftNotationParseException.java │ └── submessage │ ├── Page.java │ ├── PageReader.java │ ├── PageSeparator.java │ ├── exception │ └── PageParserException.java │ ├── field │ ├── AccountIdentification.java │ ├── ClosingAvailableBalance.java │ ├── ClosingBalance.java │ ├── DateTimeIndicator.java │ ├── FieldUtils.java │ ├── FloorLimitIndicator.java │ ├── ForwardAvailableBalance.java │ ├── GeneralField.java │ ├── InformationToAccountOwner.java │ ├── OpeningBalance.java │ ├── RelatedReference.java │ ├── StatementLine.java │ ├── StatementNumber.java │ ├── SwiftField.java │ ├── SwiftFieldReader.java │ ├── TransactionGroup.java │ ├── TransactionReferenceNumber.java │ ├── TransactionSummary.java │ ├── exception │ │ ├── FieldLineParseException.java │ │ └── FieldParseException.java │ └── subfield │ │ ├── DebitCreditMark.java │ │ ├── DebitCreditType.java │ │ ├── MessagePriority.java │ │ └── TransactionTypeIdentificationCode.java │ ├── mt940 │ ├── MT940Page.java │ └── MT940PageReader.java │ └── mt942 │ ├── MT942Page.java │ └── MT942PageReader.java └── test ├── java └── com │ └── qoomon │ └── banking │ ├── TestUtils.java │ ├── bic │ └── BICTest.java │ ├── iban │ └── IBANTest.java │ └── swift │ ├── message │ ├── SwiftMessageReaderTest.java │ └── block │ │ ├── ApplicationHeaderBlockTest.java │ │ ├── BasicHeaderBlockTest.java │ │ ├── SwiftBlockReaderTest.java │ │ ├── SystemTrailerBlockTest.java │ │ ├── TextBlockTest.java │ │ ├── UserHeaderBlockTest.java │ │ └── UserTrailerBlockTest.java │ ├── notation │ └── SwiftNotationTest.java │ └── submessage │ ├── SwiftFieldReaderTest.java │ ├── field │ ├── AccountIdentificationTest.java │ ├── BCSMessageParserTest.java │ ├── ClosingAvailableBalanceTest.java │ ├── ClosingBalanceTest.java │ ├── DateTimeIndicatorTest.java │ ├── FloorLimitIndicatorTest.java │ ├── ForwardAvailableBalanceTest.java │ ├── InformationToAccountOwnerTest.java │ ├── OpeningBalanceTest.java │ ├── RelatedReferenceTest.java │ ├── StatementLineTest.java │ ├── StatementNumberTest.java │ ├── TransactionReferenceNumberTest.java │ └── TransactionSummaryTest.java │ ├── mt940 │ └── MT940PageReaderTest.java │ └── mt942 │ └── MT942PageReaderTest.java └── resources ├── submessage ├── mt940_valid │ └── valid-mt940-content.txt └── mt942_valid │ └── valid-mt942-content.txt └── swiftmessage ├── valid-mt940.txt └── valid-mt942.txt /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Enhance GitHub Environment Variables 12 | run: echo "GITHUB_REF_NAME=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 13 | - uses: actions/checkout@v4 14 | - name: Set up JDK 1.8 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: temurin 18 | java-version: 21 19 | 20 | - name: Install CodeClimate Reporter 21 | run: | 22 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > cc-test-reporter 23 | chmod +x cc-test-reporter 24 | ./cc-test-reporter before-build 25 | 26 | - name: Build with Maven 27 | run: mvn verify -B -V 28 | 29 | - if: github.ref == 'refs/heads/main' 30 | name: Upload CodeClimate Report 31 | env: 32 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 33 | GIT_COMMIT_SHA: ${{ env.GITHUB_SHA }} 34 | GIT_BRANCH: ${{ env.GITHUB_REF_NAME }} 35 | JACOCO_SOURCE_PATH: src/main/java 36 | run: | 37 | ./cc-test-reporter format-coverage -t jacoco ./target/site/jacoco/jacoco.xml 38 | ./cc-test-reporter upload-coverage 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | *.iml 14 | .idea 15 | target 16 | src/test/resources/depositsolution 17 | src/test/java/com/depositsolution 18 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bengt Brodersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Banking Swift Messages Parser and Composer [![starline](https://starlines.qoo.monster/assets/qoomon/banking-swift-messages-java)](https://github.com/qoomon/starline) 2 | 3 | Parser for Financial SWIFT Messages 4 | SWIFT = Society for Worldwide Interbank Financial Telecommunication 5 | 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 7 | [![Build Workflow](https://github.com/qoomon/banking-swift-messages-java/workflows/Build/badge.svg)](https://github.com/qoomon/banking-swift-messages-java/actions) 8 | [![Test Coverage](https://api.codeclimate.com/v1/badges/e611239eea560ee9c72c/test_coverage)](https://codeclimate.com/github/qoomon/banking-swift-messages-java/test_coverage) 9 | 10 | ### Releases 11 | 12 | [![Release](https://jitpack.io/v/qoomon/banking-swift-messages-java.svg)](https://jitpack.io/#qoomon/banking-swift-messages-java) 13 | 14 | > [!Important] 15 | > From version `2.0.0` on Java 21 is required 16 | 17 | 18 | #### Supported Message Types (so far) 19 | * **MT940** 20 | * **MT942** 21 | 22 | If you need more MT formats just let me know and create a new [issue](https://github.com/qoomon/banking-swift-messages-java/issues) 23 | 24 | 25 | #### Usage 26 | see [tests](/src/test/java/com/qoomon/banking/swift/message/SwiftMessageReaderTest.java) 27 | 28 | 29 | ## Dev Notes 30 | [SEPA Verwendugszweck Fields](https://www.hettwer-beratung.de/sepa-spezialwissen/sepa-technische-anforderungen/sepa-gesch%C3%A4ftsvorfallcodes-gvc-mt-940/) 31 | * EREF : Ende-zu-Ende Referenz 32 | * KREF : Kundenreferenz 33 | * MREF : Mandatsreferenz 34 | * BREF : Bankreferenz 35 | * RREF : Retourenreferenz 36 | * CRED : Creditor-ID 37 | * DEBT : Debitor-ID 38 | * COAM : Zinskompensationsbetrag 39 | * OAMT : Ursprünglicher Umsatzbetrag 40 | * SVWZ : Verwendungszweck 41 | * ABWA : Abweichender Auftraggeber 42 | * ABWE : Abweichender Empfänger 43 | * IBAN : IBAN des Auftraggebers 44 | * BIC : BIC des Auftraggebers 45 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | com.qoomon.banking.swift 8 | banking-swift-messages 9 | 0.0.0-SNAPSHOT 10 | 11 | jar 12 | 13 | 14 | 15 | 21 16 | 21 17 | UTF-8 18 | UTF-8 19 | UTF-8 20 | UTF-8 21 | 22 | 23 | 24 | 25 | 26 | 27 | com.google.guava 28 | guava 29 | 33.4.6-jre 30 | 31 | 32 | 33 | org.joda 34 | joda-money 35 | 2.0.2 36 | 37 | 38 | 39 | junit 40 | junit 41 | 4.13.2 42 | test 43 | 44 | 45 | 46 | org.assertj 47 | assertj-core 48 | 3.27.3 49 | test 50 | 51 | 52 | 53 | org.mockito 54 | mockito-core 55 | 5.18.0 56 | test 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.jacoco 67 | jacoco-maven-plugin 68 | 0.8.13 69 | 70 | 71 | prepare-agent 72 | 73 | prepare-agent 74 | 75 | 76 | 77 | report 78 | test 79 | 80 | report 81 | 82 | 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-source-plugin 89 | 3.3.1 90 | 91 | 92 | attach-sources 93 | 94 | jar 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-javadoc-plugin 103 | 3.11.2 104 | 105 | 106 | attach-javadocs 107 | 108 | jar 109 | 110 | 111 | 112 | 113 | -Xdoclint:none 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/bic/BIC.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.bic; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 5 | import com.qoomon.banking.swift.notation.SwiftNotation; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | /** 11 | * Business Identifier Codes 12 | *

13 | * Format 4!a2!a2!c[3!c] 14 | *

15 | * SubFields 16 | *

17 |  * 1: 4!a   - Institution Code
18 |  * 2: 2!a   - Country Code
19 |  * 3: 2!c   - Location Code
20 |  * 4: [3!c] - Branch Code
21 |  * 
22 | * 23 | * @see http://www.sepaforcorporates.com/single-euro-payments-area/sepa-iban-number-the-definitive-guide/ 24 | * @see https://en.wikipedia.org/wiki/ISO_9362 25 | */ 26 | public class BIC { 27 | 28 | public static SwiftNotation NOTATION = new SwiftNotation("4!a2!a2!c[3!c]"); 29 | 30 | private final String institutionCode; 31 | private final String countryCode; 32 | private final String locationCode; 33 | private final Optional branchCode; 34 | 35 | public BIC(String institutionCode, String countryCode, String locationCode, String branchCode) { 36 | 37 | Preconditions.checkArgument(institutionCode != null, "institutionCode can't be null"); 38 | Preconditions.checkArgument(countryCode != null, "countryCode can't be null"); 39 | Preconditions.checkArgument(locationCode != null, "locationCode can't be null"); 40 | 41 | this.institutionCode = institutionCode; 42 | this.countryCode = countryCode; 43 | this.locationCode = locationCode; 44 | this.branchCode = Optional.ofNullable(branchCode); 45 | 46 | String bicText = this.institutionCode + this.countryCode + this.locationCode + this.branchCode.orElse(""); 47 | ensureValid(bicText); 48 | } 49 | 50 | public static BIC of(String value) { 51 | try { 52 | List subfieldList = NOTATION.parse(value); 53 | String institutionCode = subfieldList.get(0); 54 | String countryCode = subfieldList.get(1); 55 | String locationCode = subfieldList.get(2); 56 | String branchCode = subfieldList.get(3); 57 | return new BIC(institutionCode, countryCode, locationCode, branchCode); 58 | } catch (FieldNotationParseException e) { 59 | throw new IllegalArgumentException(e); 60 | } 61 | } 62 | 63 | public static void ensureValid(String bicText) { 64 | Preconditions.checkArgument(bicText != null, "bic can't be null"); 65 | try { 66 | NOTATION.parse(bicText); 67 | } catch (FieldNotationParseException e) { 68 | throw new IllegalArgumentException(e); 69 | } 70 | } 71 | 72 | public String getInstitutionCode() { 73 | return institutionCode; 74 | } 75 | 76 | public String getCountryCode() { 77 | return countryCode; 78 | } 79 | 80 | public String getLocationCode() { 81 | return locationCode; 82 | } 83 | 84 | public Optional getBranchCode() { 85 | return branchCode; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/iban/IBAN.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.iban; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 5 | import com.qoomon.banking.swift.notation.SwiftNotation; 6 | 7 | import java.math.BigInteger; 8 | import java.util.List; 9 | 10 | /** 11 | * International Bank Account Number 12 | * Format 6!a2!a2!c[3!c] 13 | *

14 | * SubFields 15 | *

 16 |  * 1: 2!a   - Country Code
 17 |  * 2: 2!n   - Check Sum
 18 |  * 3: 30!c  - BBAN (Basic Bank Account Number)
 19 |  * 
20 | * 21 | * @see http://www.sepaforcorporates.com/single-euro-payments-area/sepa-iban-number-the-definitive-guide/ 22 | * @see http://www.swift.com/dsp/resources/documents/IBAN_Registry.pdf 23 | */ 24 | public class IBAN { 25 | 26 | public static final SwiftNotation NOTATION = new SwiftNotation("2!a2!n30c"); 27 | 28 | public static final int IBAN_CHECKSUM_DIVIDEND = 97; 29 | public static final int IBAN_CHECKSUM_CHARACTER_NUMBER_OFFSET = 55; 30 | 31 | private final String countryCode; 32 | private final String checkDigits; 33 | private final String bban; 34 | 35 | public IBAN(String countryCode, String checkDigits, String bban) { 36 | 37 | Preconditions.checkArgument(countryCode != null, "countryCode can't be null"); 38 | Preconditions.checkArgument(checkDigits != null, "checkDigits can't be null"); 39 | Preconditions.checkArgument(bban != null, "bban can't be null"); 40 | 41 | this.countryCode = countryCode; 42 | this.checkDigits = checkDigits; 43 | this.bban = bban; 44 | 45 | String ibanText = this.countryCode + this.checkDigits + this.bban; 46 | ensureValid(ibanText); 47 | } 48 | 49 | public static IBAN of(String value) { 50 | // remove all whitespaces 51 | String plainValue = value.replaceAll("\\s+", ""); 52 | try { 53 | List subfieldList = NOTATION.parse(plainValue); 54 | String countryCode = subfieldList.get(0); 55 | String checkSum = subfieldList.get(1); 56 | String bban = subfieldList.get(2); 57 | 58 | return new IBAN(countryCode, checkSum, bban); 59 | } catch (FieldNotationParseException e) { 60 | throw new IllegalArgumentException(e); 61 | } 62 | } 63 | 64 | public static void ensureValid(String value) { 65 | Preconditions.checkArgument(value != null, "value can't be null"); 66 | 67 | try { 68 | List subfieldList = NOTATION.parse(value); 69 | 70 | String countryCode = subfieldList.get(0); 71 | String checkDigits = subfieldList.get(1); 72 | String bban = subfieldList.get(2); 73 | // TODO validate country specific BBAN 74 | 75 | String expectedCheckDigits = calculateDigits(countryCode, bban); 76 | if (!checkDigits.equals(expectedCheckDigits)) { 77 | throw new IllegalArgumentException("Incorrect check digits. Expected '" + expectedCheckDigits + "', but was '" + checkDigits + "'"); 78 | } 79 | } catch (FieldNotationParseException e) { 80 | throw new IllegalArgumentException(e); 81 | } 82 | } 83 | 84 | public static String calculateDigits(String countryCode, String bban) { 85 | String rearangedIban = bban + countryCode + "00"; 86 | String rearangedIbanIntegerText = replaceCharactersWithInteger(rearangedIban); 87 | BigInteger rearangedIbanInteger = new BigInteger(rearangedIbanIntegerText); 88 | int rearangedIbanModRemainder = rearangedIbanInteger.mod(BigInteger.valueOf(IBAN_CHECKSUM_DIVIDEND)).intValue(); 89 | int checkSum = 98 - rearangedIbanModRemainder; 90 | String checkDigits = String.format("%02d", checkSum); 91 | return checkDigits; 92 | } 93 | 94 | private static String replaceCharactersWithInteger(String source) { 95 | StringBuilder resultBuilder = new StringBuilder(); 96 | for (char character : source.toCharArray()) { 97 | if (Character.isLetter(character)) { 98 | resultBuilder.append((int) character - IBAN_CHECKSUM_CHARACTER_NUMBER_OFFSET); 99 | } else { 100 | resultBuilder.append(character); 101 | } 102 | } 103 | return resultBuilder.toString(); 104 | } 105 | 106 | public String getCountryCode() { 107 | return countryCode; 108 | } 109 | 110 | public String getCheckDigits() { 111 | return checkDigits; 112 | } 113 | 114 | public String getBban() { 115 | return bban; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/bcsmessage/BCSMessage.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.bcsmessage; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.ImmutableMap; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * Created by qoomon on 25/07/16 10 | *

11 | * Banking Communication Standard Format Message 12 | * http://www.ebics.de/index.php?id=77 (Anlage 3 Datenformate) 13 | * https://de.wikipedia.org/wiki/Banking_Communication_Standard 14 | * https://www.bayernlb.de/internet/media/de/internet_4/de_1/downloads_5/0800_financial_office_it_operations_5/4200_1/formate/MT940_942.pdf 15 | * https://www.ksk-koeln.de/uebersicht-mt940-geschaeftsvorfallcodes.pdfx 16 | * http://www.kontopruef.de/mt940s.shtml 17 | *

18 | * DFÜ Field Description 19 | *

20 |  * 00                                      -  Buchungstext
21 |  * 10                                      -  Primanoten-Nr.
22 |  * 20, 21, 22, 23, 24, 25, 26, 27, 28, 29  -  Verwendungszweck
23 |  * 30                                      -  Bankkennung Auftraggeber / Zahlungsempf.
24 |  * 31                                      -  Kto.Nr. Auftraggeber / Zahlungsempf.
25 |  * 32, 33                                  -  Name Auftraggeber / Zahlungsempf.
26 |  * 34                                      -  Textschlüsselergänzung
27 |  * 60, 61, 62, 63                          -  Fortsetzung Verwendungszweck
28 |  * 
29 | */ 30 | 31 | public class BCSMessage { 32 | 33 | private final String businessTransactionCode; 34 | private final Map fieldMap; 35 | 36 | public BCSMessage(String businessTransactionCode, Map fieldMap) { 37 | 38 | Preconditions.checkArgument(businessTransactionCode != null && !businessTransactionCode.isEmpty(), "businessTransactionCode can't be null or empy"); 39 | Preconditions.checkArgument(businessTransactionCode.length() == 3, "businessTransactionCode length must be 3, but was: " + businessTransactionCode); 40 | Preconditions.checkArgument(fieldMap != null, "fieldMap can't be null"); 41 | 42 | this.businessTransactionCode = businessTransactionCode; 43 | this.fieldMap = ImmutableMap.copyOf(fieldMap); 44 | } 45 | 46 | public String getBusinessTransactionCode() { 47 | return businessTransactionCode; 48 | } 49 | 50 | public Map getFieldMap() { 51 | return fieldMap; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/bcsmessage/BCSMessageParseException.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.bcsmessage; 2 | 3 | /** 4 | * Created by qoomon on 03/08/16. 5 | */ 6 | public class BCSMessageParseException extends Exception { 7 | 8 | public BCSMessageParseException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/bcsmessage/BCSMessageParser.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.bcsmessage; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | /** 9 | * Created by qoomon on 25/07/16. 10 | * http://www.kontopruef.de/mt940s.shtml 11 | *

12 | * Format '[BankTransactionCode]?[fieldId][content]?[fieldId][content]...' 13 | *

14 | */ 15 | public class BCSMessageParser { 16 | 17 | private final static Pattern BUSINESS_TRANSACTION_CODE_PATTERN = Pattern.compile("^([0-9A-Z]{3,4})(.*)", Pattern.DOTALL); 18 | /** 19 | * pattern 20 | */ 21 | private final static Pattern FIELD_PATTERN = Pattern.compile("^(.)([0-9]{2})((?:(?!\\1).)*)"); 22 | 23 | 24 | public BCSMessage parseMessage(String messageText) throws BCSMessageParseException { 25 | // join multiline to one line 26 | String oneLineMessageText = messageText.replaceAll("\\n", ""); 27 | Matcher matcher = BUSINESS_TRANSACTION_CODE_PATTERN.matcher(oneLineMessageText); 28 | if (!matcher.matches()) { 29 | throw new BCSMessageParseException("messageText " + messageText + " didn't match " + matcher.pattern()); 30 | } 31 | String messageBTC = matcher.group(1); 32 | String messageContent = matcher.group(2); 33 | 34 | Map messageFieldMap = new HashMap<>(); 35 | 36 | int parseIndex = 0; 37 | Matcher messageFieldMatcher = FIELD_PATTERN.matcher(messageContent); 38 | while (messageFieldMatcher.region(parseIndex, messageContent.length()).find()) { 39 | parseIndex = messageFieldMatcher.end(); 40 | String fieldId = messageFieldMatcher.group(2); 41 | String fieldContent = messageFieldMatcher.group(3); 42 | if(messageFieldMap.containsKey(fieldId)){ 43 | throw new BCSMessageParseException("duplicate field " + fieldId); 44 | } 45 | 46 | messageFieldMap.put(fieldId, fieldContent); 47 | } 48 | 49 | if (parseIndex != messageContent.length()) { 50 | throw new BCSMessageParseException("unparsed message part " + messageContent.substring(parseIndex)); 51 | } 52 | 53 | return new BCSMessage(messageBTC, messageFieldMap); 54 | 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/SwiftMessageReader.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.ImmutableSet; 5 | import com.qoomon.banking.swift.message.block.*; 6 | import com.qoomon.banking.swift.message.exception.SwiftMessageParseException; 7 | 8 | import java.io.Reader; 9 | import java.util.LinkedList; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | /** 14 | * Created by qoomon on 24/06/16. 15 | */ 16 | public class SwiftMessageReader { 17 | 18 | private final static Set MESSAGE_START_BLOCK_ID_SET = ImmutableSet.of(BasicHeaderBlock.BLOCK_ID_1); 19 | 20 | private final SwiftBlockReader blockReader; 21 | 22 | private GeneralBlock currentBlock = null; 23 | private GeneralBlock nextBlock = null; 24 | 25 | 26 | public SwiftMessageReader(Reader textReader) { 27 | 28 | Preconditions.checkArgument(textReader != null, "textReader can't be null"); 29 | 30 | this.blockReader = new SwiftBlockReader(textReader); 31 | } 32 | 33 | public List readAll() throws SwiftMessageParseException { 34 | List result = new LinkedList<>(); 35 | SwiftMessage message; 36 | while ((message = read()) != null) { 37 | result.add(message); 38 | } 39 | return result; 40 | } 41 | 42 | public SwiftMessage read() throws SwiftMessageParseException { 43 | try { 44 | if (currentBlock == null) { 45 | nextBlock = blockReader.readBlock(); 46 | } 47 | 48 | SwiftMessage message = null; 49 | 50 | // message fields (builder) // TODO create builder 51 | BasicHeaderBlock messageBuilderBasicHeaderBlock = null; 52 | ApplicationHeaderBlock messageBuilderApplicationHeaderBlock = null; 53 | UserHeaderBlock messageBuilderUserHeaderBlock = null; 54 | TextBlock messageBuilderTextBlock = null; 55 | UserTrailerBlock messageBuilderUserTrailerBlock = null; 56 | SystemTrailerBlock messageBuilderSystemTrailerBlock = null; 57 | 58 | Set nextValidBlockIdSet = MESSAGE_START_BLOCK_ID_SET; 59 | 60 | while (message == null && nextBlock != null) { 61 | 62 | ensureValidNextBlock(nextBlock, nextValidBlockIdSet, blockReader); 63 | 64 | currentBlock = nextBlock; 65 | nextBlock = blockReader.readBlock(); 66 | 67 | switch (currentBlock.getId()) { 68 | case BasicHeaderBlock.BLOCK_ID_1: { 69 | messageBuilderBasicHeaderBlock = BasicHeaderBlock.of(currentBlock); 70 | nextValidBlockIdSet = ImmutableSet.of(ApplicationHeaderBlock.BLOCK_ID_2); 71 | break; 72 | } 73 | case ApplicationHeaderBlock.BLOCK_ID_2: { 74 | messageBuilderApplicationHeaderBlock = ApplicationHeaderBlock.of(currentBlock); 75 | nextValidBlockIdSet = ImmutableSet.of(UserHeaderBlock.BLOCK_ID_3, TextBlock.BLOCK_ID_4); 76 | break; 77 | } 78 | case UserHeaderBlock.BLOCK_ID_3: { 79 | messageBuilderUserHeaderBlock = UserHeaderBlock.of(currentBlock); 80 | nextValidBlockIdSet = ImmutableSet.of(TextBlock.BLOCK_ID_4); 81 | break; 82 | } 83 | case TextBlock.BLOCK_ID_4: { 84 | messageBuilderTextBlock = TextBlock.of(currentBlock); 85 | nextValidBlockIdSet = ImmutableSet.of(UserTrailerBlock.BLOCK_ID_5, SystemTrailerBlock.BLOCK_ID_S); 86 | break; 87 | } 88 | case UserTrailerBlock.BLOCK_ID_5: { 89 | messageBuilderUserTrailerBlock = UserTrailerBlock.of(currentBlock); 90 | nextValidBlockIdSet = ImmutableSet.of(SystemTrailerBlock.BLOCK_ID_S); 91 | break; 92 | } 93 | case SystemTrailerBlock.BLOCK_ID_S: { 94 | messageBuilderSystemTrailerBlock = SystemTrailerBlock.of(currentBlock); 95 | nextValidBlockIdSet = ImmutableSet.of(); 96 | break; 97 | } 98 | default: 99 | throw new SwiftMessageParseException("unexpected block id '" + currentBlock.getId() + "'", blockReader.getLineNumber()); 100 | } 101 | 102 | // finish message 103 | if (nextBlock == null || MESSAGE_START_BLOCK_ID_SET.contains(nextBlock.getId())) { 104 | message = new SwiftMessage( 105 | messageBuilderBasicHeaderBlock, 106 | messageBuilderApplicationHeaderBlock, 107 | messageBuilderUserHeaderBlock, 108 | messageBuilderTextBlock, 109 | messageBuilderUserTrailerBlock, 110 | messageBuilderSystemTrailerBlock); 111 | } 112 | } 113 | 114 | return message; 115 | } catch (SwiftMessageParseException e) { 116 | throw e; 117 | } catch (Exception e) { 118 | throw new SwiftMessageParseException(e.getMessage(), blockReader.getLineNumber(), e); 119 | } 120 | } 121 | 122 | private void ensureValidNextBlock(GeneralBlock block, Set expectedBlockIdSet, SwiftBlockReader blockReader) throws SwiftMessageParseException { 123 | String blockId = block != null ? block.getId() : null; 124 | if (!expectedBlockIdSet.contains(blockId)) { 125 | throw new SwiftMessageParseException("Expected Block '" + expectedBlockIdSet + "', but was '" + blockId + "'", blockReader.getLineNumber()); 126 | } 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/ApplicationHeaderBlock.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.qoomon.banking.swift.message.block.exception.BlockFieldParseException; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * Input Application Header Block 10 | *

11 | * Fixed Length Format 12 | *

13 |  * 1:  1  - Mode - I = Input, O = Output
14 |  * ...
15 |  * ..
16 |  * .
17 |  * 
18 | * 19 | * @see ApplicationHeaderInputBlock 20 | * @see ApplicationHeaderOutputBlock 21 | */ 22 | public class ApplicationHeaderBlock implements SwiftBlock { 23 | 24 | public static final String BLOCK_ID_2 = "2"; 25 | 26 | public final Type type; 27 | 28 | private final Optional input; 29 | 30 | private final Optional output; 31 | 32 | 33 | public ApplicationHeaderBlock(ApplicationHeaderInputBlock input) { 34 | type = Type.INPUT; 35 | this.input = Optional.of(input); 36 | this.output = Optional.empty(); 37 | } 38 | 39 | public ApplicationHeaderBlock(ApplicationHeaderOutputBlock output) { 40 | type = Type.OUTPUT; 41 | this.input = Optional.empty(); 42 | this.output = Optional.of(output); 43 | } 44 | 45 | public static ApplicationHeaderBlock of(GeneralBlock block) throws BlockFieldParseException { 46 | Preconditions.checkArgument(block.getId().equals(BLOCK_ID_2), "unexpected block id '%s'", block.getId()); 47 | 48 | if (block.getContent().startsWith("I")) { 49 | ApplicationHeaderInputBlock input = ApplicationHeaderInputBlock.of(block); 50 | return new ApplicationHeaderBlock(input); 51 | } 52 | 53 | if (block.getContent().startsWith("O")) { 54 | ApplicationHeaderOutputBlock output = ApplicationHeaderOutputBlock.of(block); 55 | return new ApplicationHeaderBlock(output); 56 | } 57 | 58 | throw new IllegalArgumentException("Block '" + block.getId() + "' unknown Type " + ""); 59 | 60 | } 61 | 62 | public Optional getInput() { 63 | return input; 64 | } 65 | 66 | public Optional getOutput() { 67 | return output; 68 | } 69 | 70 | public Type getType() { 71 | return type; 72 | } 73 | 74 | @Override 75 | public String getId() { 76 | return BLOCK_ID_2; 77 | } 78 | 79 | @Override 80 | public String getContent() { 81 | if (getInput().isPresent()){ 82 | return getInput().get().getContent(); 83 | } else { 84 | return getOutput().get().getContent(); 85 | } 86 | } 87 | 88 | enum Type { 89 | INPUT, 90 | OUTPUT 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/ApplicationHeaderInputBlock.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.qoomon.banking.swift.message.block.exception.BlockFieldParseException; 5 | import com.qoomon.banking.swift.submessage.field.subfield.MessagePriority; 6 | 7 | import java.util.Optional; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | 11 | /** 12 | * Input Application Header Block 13 | *

14 | * Fixed Length Format 15 | *

 16 |  * 1:  1  - Mode - I = Input
 17 |  * 2:  3  - Message Type MTxxx e.g. 940
 18 |  * 3: 12  - Receiver's address with X in position 9 It is padded with Xs if no branch is required. Typically 8 - BIC, 1 - 'X', 3 - Branch Code
 19 |  * 4:  1  - Message Priority - U = Urgent, N = Normal, S = System
 20 |  * 5:  1  - Delivery Monitoring - Optional
 21 |  * 6:  3  - Obsolescence Period - Optional
 22 |  * 
23 | * Example
24 | * I00BANKDEFFXXXXU3003 25 | * 26 | * @see https://www.ibm.com/support/knowledgecenter/SSBTEG_4.3.0/com.ibm.wbia_adapters.doc/doc/swift/swift72.htm 27 | */ 28 | public class ApplicationHeaderInputBlock { 29 | 30 | public static final String MODE_CODE = "I"; 31 | 32 | public static final Pattern BLOCK_CONTENT_PATTERN = Pattern.compile("(I)(.{3})(.{12})(.{1})(.{1})?(.{3})?"); 33 | 34 | private final String messageType; 35 | 36 | private final String receiverAddress; 37 | 38 | private final MessagePriority messagePriority; 39 | 40 | private final Optional deliveryMonitoring; 41 | 42 | private final Optional obsolescencePeriod; 43 | 44 | 45 | public ApplicationHeaderInputBlock(String messageType, String receiverAddress, MessagePriority messagePriority, String deliveryMonitoring, String obsolescencePeriod) { 46 | 47 | Preconditions.checkArgument(messageType != null, "messageType can't be null"); 48 | Preconditions.checkArgument(receiverAddress != null, "receiverAddress can't be null"); 49 | Preconditions.checkArgument(messagePriority != null, "messagePriority can't be null"); 50 | 51 | this.messageType = messageType; 52 | this.receiverAddress = receiverAddress; 53 | this.messagePriority = messagePriority; 54 | this.deliveryMonitoring = Optional.ofNullable(deliveryMonitoring); 55 | this.obsolescencePeriod = Optional.ofNullable(obsolescencePeriod); 56 | } 57 | 58 | public static ApplicationHeaderInputBlock of(GeneralBlock block) throws BlockFieldParseException { 59 | Preconditions.checkArgument(block.getId().equals(ApplicationHeaderBlock.BLOCK_ID_2), "unexpected block id '%s'", block.getId()); 60 | 61 | Matcher blockContentMatcher = BLOCK_CONTENT_PATTERN.matcher(block.getContent()); 62 | if (!blockContentMatcher.matches()) { 63 | throw new BlockFieldParseException("Block '" + block.getId() + "' content did not match format " + BLOCK_CONTENT_PATTERN); 64 | } 65 | 66 | String mode = blockContentMatcher.group(1); 67 | if (!mode.equals(MODE_CODE)) { 68 | throw new BlockFieldParseException("Block '" + block.getId() + "' expect mod '" + MODE_CODE + "', but was " + mode); 69 | } 70 | 71 | String messageType = blockContentMatcher.group(2); 72 | String receiverAddress = blockContentMatcher.group(3); 73 | MessagePriority messagePriority = MessagePriority.of(blockContentMatcher.group(4)); 74 | String deliveryMonitoring = blockContentMatcher.group(5); 75 | String obsolescencePeriod = blockContentMatcher.group(6); 76 | 77 | return new ApplicationHeaderInputBlock(messageType, receiverAddress, messagePriority, deliveryMonitoring, obsolescencePeriod); 78 | } 79 | 80 | public String getMessageType() { 81 | return messageType; 82 | } 83 | 84 | public String getReceiverAddress() { 85 | return receiverAddress; 86 | } 87 | 88 | public MessagePriority getMessagePriority() { 89 | return messagePriority; 90 | } 91 | 92 | public Optional getDeliveryMonitoring() { 93 | return deliveryMonitoring; 94 | } 95 | 96 | public Optional getObsolescencePeriod() { 97 | return obsolescencePeriod; 98 | } 99 | 100 | public String getContent() { 101 | StringBuilder contentBuilder = new StringBuilder(); 102 | contentBuilder.append(MODE_CODE); 103 | contentBuilder.append(messageType); 104 | contentBuilder.append(receiverAddress); 105 | contentBuilder.append(messagePriority.asText()); 106 | if (deliveryMonitoring.isPresent()) { 107 | contentBuilder.append(deliveryMonitoring.get()); 108 | } 109 | if (obsolescencePeriod.isPresent()) { 110 | contentBuilder.append(obsolescencePeriod.get()); 111 | } 112 | return contentBuilder.toString(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/ApplicationHeaderOutputBlock.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.qoomon.banking.swift.message.block.exception.BlockFieldParseException; 5 | import com.qoomon.banking.swift.submessage.field.subfield.MessagePriority; 6 | 7 | import java.time.LocalDateTime; 8 | import java.time.format.DateTimeFormatter; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | /** 13 | * Output Application Header Block 14 | *

15 | * Fixed Length Format 16 | *

 17 |  *  1:  1  - Mode - O = Output
 18 |  *  2:  3  - Message Type MTxxx e.g. 940
 19 |  *  3:  4  - Input time with respect to the sender
 20 |  *  4:  6  - Input date with respect to the sender
 21 |  *  5: 12  - The Message Input Reference (MIR), with Sender's address
 22 |  *  6:  4  - Session number
 23 |  *  7:  6  — Sequence number
 24 |  *  8:  6  - Output date with respect to Receiver
 25 |  *  9:  4  - Output time with respect to Receiver
 26 |  * 10:  1  - Message Priority - U = Urgent, N = Normal, S = System
 27 |  * 
28 | * Example
29 | * O1001200970103BANKBEBBAXXX22221234569701031201N 30 | * 31 | * @see https://www.ibm.com/support/knowledgecenter/SSBTEG_4.3.0/com.ibm.wbia_adapters.doc/doc/swift/swift72.htm 32 | */ 33 | public class ApplicationHeaderOutputBlock { 34 | 35 | public static final String MODE_CODE = "O"; 36 | 37 | public static final Pattern BLOCK_CONTENT_PATTERN = Pattern.compile("(O)(.{3})(.{4})(.{6})(.{12})(.{4})(.{6})(.{6})(.{4})(.?)"); 38 | 39 | private static final DateTimeFormatter INPUT_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("HHmmyyMMdd"); 40 | 41 | private static final DateTimeFormatter OUTPUT_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyMMddHHmm"); 42 | 43 | private final String sessionNumber; 44 | 45 | private final String sequenceNumber; 46 | 47 | private final String messageType; 48 | 49 | private final LocalDateTime inputDateTime; 50 | 51 | private final String inputReference; 52 | 53 | private final LocalDateTime outputDateTime; 54 | 55 | private final MessagePriority messagePriority; 56 | 57 | 58 | public ApplicationHeaderOutputBlock(String sessionNumber, String sequenceNumber, String messageType, LocalDateTime inputDateTime, String inputReference, LocalDateTime outputDateTime, MessagePriority messagePriority) { 59 | 60 | Preconditions.checkArgument(sessionNumber != null, "sessionNumber can't be null"); 61 | Preconditions.checkArgument(sequenceNumber != null, "sequenceNumber can't be null"); 62 | Preconditions.checkArgument(messageType != null, "messageType can't be null"); 63 | Preconditions.checkArgument(inputDateTime != null, "inputDateTime can't be null"); 64 | Preconditions.checkArgument(inputReference != null, "inputReference can't be null"); 65 | Preconditions.checkArgument(outputDateTime != null, "outputDateTime can't be null"); 66 | Preconditions.checkArgument(messagePriority != null, "messagePriority can't be null"); 67 | 68 | this.sessionNumber = sessionNumber; 69 | this.sequenceNumber = sequenceNumber; 70 | this.messageType = messageType; 71 | this.inputDateTime = inputDateTime; 72 | this.inputReference = inputReference; 73 | this.outputDateTime = outputDateTime; 74 | this.messagePriority = messagePriority; 75 | } 76 | 77 | public static ApplicationHeaderOutputBlock of(GeneralBlock block) throws BlockFieldParseException { 78 | Preconditions.checkArgument(block.getId().equals(ApplicationHeaderBlock.BLOCK_ID_2), "unexpected block id '%s'", block.getId()); 79 | 80 | Matcher blockContentMatcher = BLOCK_CONTENT_PATTERN.matcher(block.getContent()); 81 | if (!blockContentMatcher.matches()) { 82 | throw new BlockFieldParseException("Block '" + block.getId() + "' content did not match format " + BLOCK_CONTENT_PATTERN); 83 | } 84 | 85 | String mode = blockContentMatcher.group(1); 86 | if (!mode.equals(MODE_CODE)) { 87 | throw new BlockFieldParseException("Block '" + block.getId() + "' expect mod '" + MODE_CODE + "', but was " + mode); 88 | } 89 | 90 | String messageType = blockContentMatcher.group(2); 91 | LocalDateTime inputDateTime = LocalDateTime.parse(blockContentMatcher.group(3) + blockContentMatcher.group(4), INPUT_DATE_TIME_FORMATTER); 92 | String inputReference = blockContentMatcher.group(5); 93 | String sessionNumber = blockContentMatcher.group(6); 94 | String sequenceNumber = blockContentMatcher.group(7); 95 | LocalDateTime outputDateTime = LocalDateTime.parse(blockContentMatcher.group(8) + blockContentMatcher.group(9), OUTPUT_DATE_TIME_FORMATTER); 96 | MessagePriority messagePriority = MessagePriority.of(blockContentMatcher.group(10)); 97 | 98 | return new ApplicationHeaderOutputBlock(sessionNumber, sequenceNumber, messageType, inputDateTime, inputReference, outputDateTime, messagePriority); 99 | } 100 | 101 | 102 | public String getMessageType() { 103 | return messageType; 104 | } 105 | 106 | public LocalDateTime getInputDateTime() { 107 | return inputDateTime; 108 | } 109 | 110 | public String getInputReference() { 111 | return inputReference; 112 | } 113 | 114 | public LocalDateTime getOutputDateTime() { 115 | return outputDateTime; 116 | } 117 | 118 | public MessagePriority getMessagePriority() { 119 | return messagePriority; 120 | } 121 | 122 | public String getSessionNumber() { 123 | return sessionNumber; 124 | } 125 | 126 | public String getSequenceNumber() { 127 | return sequenceNumber; 128 | } 129 | 130 | public String getContent() { 131 | StringBuilder contentBuilder = new StringBuilder(); 132 | contentBuilder.append(MODE_CODE); 133 | contentBuilder.append(messageType); 134 | contentBuilder.append(INPUT_DATE_TIME_FORMATTER.format(inputDateTime)); 135 | contentBuilder.append(inputReference); 136 | contentBuilder.append(sessionNumber); 137 | contentBuilder.append(sequenceNumber); 138 | contentBuilder.append(OUTPUT_DATE_TIME_FORMATTER.format(outputDateTime)); 139 | contentBuilder.append(messagePriority.asText()); 140 | return contentBuilder.toString(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/BasicHeaderBlock.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.qoomon.banking.swift.message.block.exception.BlockFieldParseException; 5 | 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | /** 10 | * Basic Header Block 11 | *

12 | * Fixed Length Format 13 | *

 14 |  * 1:  1  - Indicates the Application Id - F = FIN (financial application), A = GPA (general purpose application), L = GPA (for logins, and so on)
 15 |  * 2:  2  - Indicates the Service Id -  01 = FIN/GPA, 21 = ACK/NAK
 16 |  * 3: 12  - Logical terminal (LT) address, which is typically 8 - BIC, 1 - Logical Terminal Code, 3 - Branch Code
 17 |  * 4:  4  - Session number - It is generated by the user's computer and is padded with zeros.
 18 |  * 5:  6  - Sequence number - It is generated by the user's computer. It is padded with zeros.
 19 |  * 
20 | * Example
21 | * F01YOURCODEZABC1234567890 22 | * 23 | * @see https://www.ibm.com/support/knowledgecenter/SSBTEG_4.3.0/com.ibm.wbia_adapters.doc/doc/swift/swift72.htm 24 | */ 25 | public class BasicHeaderBlock implements SwiftBlock { 26 | 27 | public static final String BLOCK_ID_1 = "1"; 28 | 29 | public static final Pattern BLOCK_CONTENT_PATTERN = Pattern.compile("(.{1})(.{2})(.{12})(.{4})(.{6})"); 30 | 31 | private final String applicationId; 32 | 33 | private final String serviceId; 34 | 35 | private final String logicalTerminalAddress; 36 | 37 | private final String sessionNumber; 38 | 39 | private final String sequenceNumber; 40 | 41 | 42 | public BasicHeaderBlock(String applicationId, String serviceId, String logicalTerminalAddress, String sessionNumber, String sequenceNumber) { 43 | 44 | Preconditions.checkArgument(applicationId != null, "applicationId can't be null"); 45 | Preconditions.checkArgument(serviceId != null, "serviceId can't be null"); 46 | Preconditions.checkArgument(logicalTerminalAddress != null, "logicalTerminalAddress can't be null"); 47 | Preconditions.checkArgument(sessionNumber != null, "sessionNumber can't be null"); 48 | Preconditions.checkArgument(sequenceNumber != null, "sequenceNumber can't be null"); 49 | 50 | this.applicationId = applicationId; 51 | this.serviceId = serviceId; 52 | this.logicalTerminalAddress = logicalTerminalAddress; 53 | this.sessionNumber = sessionNumber; 54 | this.sequenceNumber = sequenceNumber; 55 | } 56 | 57 | public static BasicHeaderBlock of(GeneralBlock block) throws BlockFieldParseException { 58 | Preconditions.checkArgument(block.getId().equals(BLOCK_ID_1), "unexpected block id '%s'", block.getId()); 59 | 60 | Matcher blockContentMatcher = BLOCK_CONTENT_PATTERN.matcher(block.getContent()); 61 | if (!blockContentMatcher.matches()) { 62 | throw new BlockFieldParseException("Block '" + block.getId() + "' content did not match format " + BLOCK_CONTENT_PATTERN); 63 | } 64 | 65 | String applicationId = blockContentMatcher.group(1); 66 | String serviceId = blockContentMatcher.group(2); 67 | String logicalTerminalAddress = blockContentMatcher.group(3); 68 | String sessionNumber = blockContentMatcher.group(4); 69 | String sequenceNumber = blockContentMatcher.group(5); 70 | 71 | return new BasicHeaderBlock(applicationId, serviceId, logicalTerminalAddress, sessionNumber, sequenceNumber); 72 | } 73 | 74 | public String getApplicationId() { 75 | return applicationId; 76 | } 77 | 78 | public String getServiceId() { 79 | return serviceId; 80 | } 81 | 82 | public String getLogicalTerminalAddress() { 83 | return logicalTerminalAddress; 84 | } 85 | 86 | public String getSessionNumber() { 87 | return sessionNumber; 88 | } 89 | 90 | public String getSequenceNumber() { 91 | return sequenceNumber; 92 | } 93 | 94 | @Override 95 | public String getId() { 96 | return BLOCK_ID_1; 97 | } 98 | 99 | @Override 100 | public String getContent() { 101 | return applicationId + serviceId + logicalTerminalAddress + sessionNumber + sequenceNumber; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/BlockUtils.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | 4 | /** 5 | * Created by qoomon on 26/08/16. 6 | */ 7 | public final class BlockUtils { 8 | 9 | /** 10 | * Convert to Swift Text Format. 11 | * 12 | * @param block to convert 13 | * @return swift text 14 | */ 15 | public static String swiftTextOf(SwiftBlock block) { 16 | return swiftTextOf(block.getId(), block.getContent()); 17 | } 18 | 19 | public static String swiftTextOf(String id, String content) { 20 | return "{" + id + ":" + content + "}"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/GeneralBlock.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.google.common.base.Preconditions; 4 | 5 | /** 6 | * Created by qoomon on 07/07/16. 7 | */ 8 | public class GeneralBlock implements SwiftBlock { 9 | 10 | private final String id; 11 | private final String content; 12 | 13 | public GeneralBlock(String id, String content) { 14 | 15 | Preconditions.checkArgument(id != null && !id.isEmpty(), "id can't be null or empty"); 16 | Preconditions.checkArgument(content != null, "content can't be null"); 17 | 18 | this.id = id; 19 | this.content = content; 20 | } 21 | 22 | @Override 23 | public String getId() { 24 | return id; 25 | } 26 | 27 | @Override 28 | public String getContent() { 29 | return content; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/SwiftBlock.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | /** 4 | * Created by qoomon on 26/08/16. 5 | */ 6 | public interface SwiftBlock { 7 | 8 | String getId(); 9 | 10 | String getContent(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/SwiftBlockReader.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.qoomon.banking.swift.message.block.exception.BlockParseException; 5 | 6 | import java.io.IOException; 7 | import java.io.Reader; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | 11 | import static java.lang.Character.isWhitespace; 12 | 13 | /** 14 | * Created by qoomon on 07/07/16. 15 | */ 16 | public class SwiftBlockReader { 17 | 18 | private static final char END_OF_STREAM = (char) -1; 19 | 20 | private static final Pattern BLOCK_PATTERN = Pattern.compile("^\\{(?[^:]+):(?.*)}", Pattern.DOTALL); 21 | 22 | private final Reader textReader; 23 | 24 | private int lineNumber = 1; 25 | private int lineCharIndex = 0; 26 | private int openingBrackets = 0; 27 | private int closingBrackets = 0; 28 | 29 | public SwiftBlockReader(Reader textReader) { 30 | 31 | Preconditions.checkArgument(textReader != null, "textReader can't be null"); 32 | 33 | this.textReader = textReader; 34 | } 35 | 36 | public GeneralBlock readBlock() throws BlockParseException { 37 | 38 | GeneralBlock block = null; 39 | StringBuilder blockBuilder = new StringBuilder(); 40 | 41 | try { 42 | char messageCharacter; 43 | while (block == null && (messageCharacter = (char) textReader.read()) != END_OF_STREAM) { 44 | 45 | if (messageCharacter == '\r') { 46 | continue; 47 | } 48 | 49 | // increment line index 50 | if (messageCharacter == '\n') { 51 | lineNumber++; 52 | lineCharIndex = 0; 53 | } 54 | 55 | lineCharIndex++; 56 | 57 | if (blockBuilder.length() == 0 && messageCharacter != '{') { 58 | if (isWhitespace(messageCharacter)) { 59 | // ignore whitespaces between blocks 60 | continue; 61 | } else if (messageCharacter == '}') { 62 | throw new BlockParseException("Found closing bracket without preceding opening bracket", lineNumber); 63 | } else { 64 | throw new BlockParseException("No characters are allowed outside of blocks, but was: '" + messageCharacter + "'", lineNumber); 65 | } 66 | } 67 | 68 | if (messageCharacter == '{') { 69 | openingBrackets++; 70 | } 71 | if (messageCharacter == '}') { 72 | closingBrackets++; 73 | } 74 | 75 | blockBuilder.append(messageCharacter); 76 | 77 | if (openingBrackets == closingBrackets) { 78 | 79 | Matcher blockMatcher = BLOCK_PATTERN.matcher(blockBuilder.toString()); 80 | if (!blockMatcher.matches()) { 81 | if (openingBrackets != 0) { 82 | throw new BlockParseException("Unexpected block structure", lineNumber); 83 | } else { 84 | throw new BlockParseException("Unexpected block structure start", lineNumber); 85 | } 86 | } 87 | 88 | String blockId = blockMatcher.group("id"); 89 | String blockContent = blockMatcher.group("content"); 90 | block = new GeneralBlock(blockId, blockContent); 91 | 92 | //reset block building 93 | blockBuilder = new StringBuilder(); 94 | openingBrackets = 0; 95 | closingBrackets = 0; 96 | } 97 | } 98 | } catch (IOException e) { 99 | throw new BlockParseException(e); 100 | } 101 | 102 | if (openingBrackets != closingBrackets) { 103 | throw new BlockParseException("Unclosed '{'", lineNumber); 104 | } 105 | 106 | return block; 107 | } 108 | 109 | public int getLineNumber() { 110 | return lineNumber; 111 | } 112 | 113 | public int getLineCharIndex() { 114 | return lineCharIndex; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/SystemTrailerBlock.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.qoomon.banking.swift.message.block.exception.BlockFieldParseException; 5 | import com.qoomon.banking.swift.message.block.exception.BlockParseException; 6 | 7 | import java.io.StringReader; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | 12 | /** 13 | * System Trail Block 14 | *

15 | * System trailers convey additional or special details about the SWIFT message. If any of the first three system trailers are present, they occur in the following order. The remaining system trailers can occur in any order. 16 | *

17 | * Sub Blocks 18 | *

 19 |  * 1: CHK - Checksum
 20 |  * 2: SYS - System Originated Message
 21 |  * 3: TNG - Training
 22 |  * 4: PDM - Possible Duplicate Message
 23 |  * 5: DLM - Delayed Message
 24 |  * 6: MRF - Message Reference
 25 |  * 
26 | * 27 | * @see https://msdn.microsoft.com/en-us/library/ee350615.aspx 28 | */ 29 | public class SystemTrailerBlock implements SwiftBlock { 30 | 31 | public static final String BLOCK_ID_S = "S"; 32 | 33 | private final Optional checksum; 34 | 35 | private final Optional systemOriginatedMessage; 36 | 37 | private final Optional training; 38 | 39 | private final Optional possibleDuplicateMessage; 40 | 41 | private final Optional delayedMessage; 42 | 43 | private final Optional messageReference; 44 | 45 | 46 | private final Map additionalSubblocks; 47 | 48 | public SystemTrailerBlock(String checksum, 49 | String systemOriginatedMessage, 50 | String training, 51 | String possibleDuplicateMessage, 52 | String delayedMessage, 53 | String messageReference, 54 | Map additionalSubblocks) { 55 | 56 | Preconditions.checkArgument(additionalSubblocks != null, "additionalSubblocks can't be null"); 57 | 58 | this.checksum = Optional.ofNullable(checksum); 59 | this.systemOriginatedMessage = Optional.ofNullable(systemOriginatedMessage); 60 | this.training = Optional.ofNullable(training); 61 | this.possibleDuplicateMessage = Optional.ofNullable(possibleDuplicateMessage); 62 | this.delayedMessage = Optional.ofNullable(delayedMessage); 63 | this.messageReference = Optional.ofNullable(messageReference); 64 | this.additionalSubblocks = additionalSubblocks; 65 | } 66 | 67 | 68 | public static SystemTrailerBlock of(GeneralBlock block) throws BlockFieldParseException { 69 | Preconditions.checkArgument(block.getId().equals(BLOCK_ID_S), "unexpected block id '%s'", block.getId()); 70 | 71 | SwiftBlockReader blockReader = new SwiftBlockReader(new StringReader(block.getContent())); 72 | 73 | String checksum = null; 74 | String systemOriginatedMessage = null; 75 | String training = null; 76 | String possibleDuplicateMessage = null; 77 | String delayedMessage = null; 78 | String messageReference = null; 79 | Map additionalSubblocks = new HashMap<>(); 80 | 81 | try { 82 | GeneralBlock subblock; 83 | while ((subblock = blockReader.readBlock()) != null) { 84 | switch (subblock.getId()) { 85 | case "CHK": 86 | checksum = subblock.getContent(); // TODO regex check 87 | break; 88 | case "SYS": 89 | systemOriginatedMessage = subblock.getContent(); // TODO regex check 90 | break; 91 | case "TNG": 92 | training = subblock.getContent(); // TODO regex check 93 | break; 94 | case "PDM": 95 | possibleDuplicateMessage = subblock.getContent(); // TODO regex check 96 | break; 97 | case "DLM": 98 | messageReference = subblock.getContent(); // TODO regex check 99 | break; 100 | case "MRF": 101 | messageReference = subblock.getContent(); // TODO regex check 102 | break; 103 | default: 104 | additionalSubblocks.put(subblock.getId(), subblock); 105 | break; 106 | } 107 | } 108 | } catch (BlockParseException e) { 109 | throw new BlockFieldParseException("Block '" + block.getId() + "' content error", e); 110 | } 111 | 112 | return new SystemTrailerBlock( 113 | checksum, 114 | systemOriginatedMessage, 115 | training, 116 | possibleDuplicateMessage, 117 | delayedMessage, 118 | messageReference, 119 | additionalSubblocks 120 | ); 121 | } 122 | 123 | public Optional getChecksum() { 124 | return checksum; 125 | } 126 | 127 | public Optional getSystemOriginatedMessage() { 128 | return systemOriginatedMessage; 129 | } 130 | 131 | public Optional getTraining() { 132 | return training; 133 | } 134 | 135 | public Optional getPossibleDuplicateMessage() { 136 | return possibleDuplicateMessage; 137 | } 138 | 139 | public Optional getDelayedMessage() { 140 | return delayedMessage; 141 | } 142 | 143 | public Optional getMessageReference() { 144 | return messageReference; 145 | } 146 | 147 | public GeneralBlock getAdditionalSubblocks(String id) { 148 | return additionalSubblocks.get(id); 149 | } 150 | 151 | @Override 152 | public String getId() { 153 | return BLOCK_ID_S; 154 | } 155 | 156 | @Override 157 | public String getContent() { 158 | StringBuilder contentBuilder = new StringBuilder(); 159 | if(checksum.isPresent()) { 160 | contentBuilder.append(BlockUtils.swiftTextOf("CHK", checksum.get())); 161 | } 162 | if(systemOriginatedMessage.isPresent()) { 163 | contentBuilder.append(BlockUtils.swiftTextOf("SYS", systemOriginatedMessage.get())); 164 | } 165 | if(training.isPresent()) { 166 | contentBuilder.append(BlockUtils.swiftTextOf("TNG", training.get())); 167 | } 168 | if(possibleDuplicateMessage.isPresent()) { 169 | contentBuilder.append(BlockUtils.swiftTextOf("PDM", possibleDuplicateMessage.get())); 170 | } 171 | if(delayedMessage.isPresent()) { 172 | contentBuilder.append(BlockUtils.swiftTextOf("DLM", delayedMessage.get())); 173 | } 174 | if(messageReference.isPresent()) { 175 | contentBuilder.append(BlockUtils.swiftTextOf("MRF", messageReference.get())); 176 | } 177 | for (GeneralBlock subblock : additionalSubblocks.values()) { 178 | contentBuilder.append(BlockUtils.swiftTextOf(subblock.getId(), subblock.getContent())); 179 | } 180 | return contentBuilder.toString(); 181 | } 182 | } 183 | 184 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/TextBlock.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.qoomon.banking.swift.message.block.exception.BlockFieldParseException; 5 | 6 | import java.util.Optional; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | public class TextBlock implements SwiftBlock { 11 | 12 | public static final String BLOCK_ID_4 = "4"; 13 | 14 | public static final Pattern FIELD_PATTERN = Pattern.compile("([^\\n]+)?\\n((?>:?.*\\n)*+-)"); 15 | 16 | private final Optional infoLine; 17 | 18 | private final String text; 19 | 20 | 21 | public TextBlock(String infoLine, String text) { 22 | Preconditions.checkArgument(text != null, "content can't be null"); 23 | 24 | this.infoLine = Optional.ofNullable(infoLine); 25 | this.text = text; 26 | } 27 | 28 | public static TextBlock of(GeneralBlock block) throws BlockFieldParseException { 29 | Preconditions.checkArgument(block.getId().equals(BLOCK_ID_4), "unexpected block id '{}'", block.getId()); 30 | 31 | Matcher blockMatcher = FIELD_PATTERN.matcher(block.getContent()); 32 | if (!blockMatcher.matches()) { 33 | throw new BlockFieldParseException("Block " + BLOCK_ID_4 + " did not match pattern " + FIELD_PATTERN); 34 | } 35 | // remove first empty line 36 | String infoLine = blockMatcher.group(1); 37 | String text = blockMatcher.group(2); 38 | 39 | return new TextBlock(infoLine, text); 40 | } 41 | 42 | public Optional getInfoLine() { 43 | return infoLine; 44 | } 45 | 46 | @Override 47 | public String getId() { 48 | return BLOCK_ID_4; 49 | } 50 | 51 | public String getText() { 52 | return text; 53 | } 54 | 55 | @Override 56 | public String getContent() { 57 | StringBuilder contentBuilder = new StringBuilder(); 58 | if (infoLine.isPresent()) { 59 | contentBuilder.append(infoLine.get()); 60 | } 61 | contentBuilder.append("\n").append(text); 62 | return contentBuilder.toString(); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/UserHeaderBlock.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.qoomon.banking.swift.message.block.exception.BlockFieldParseException; 6 | import com.qoomon.banking.swift.message.block.exception.BlockParseException; 7 | 8 | import java.io.StringReader; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Optional; 12 | 13 | /** 14 | * User Header Block 15 | *

16 | * Sub Blocks 17 | *

 18 |  * 1: 113 - Banking Priority Code of 4 alphanumeric characters - Optional
 19 |  * 2: 108 - Indicates the Message User Reference (MUR) value, which can be up to 16 characters, and will be returned in the ACK
 20 |  * 
21 | * Example
22 | * {113:SEPA}{108:ILOVESEPA} 23 | * 24 | * @see https://www.ibm.com/support/knowledgecenter/SSBTEG_4.3.0/com.ibm.wbia_adapters.doc/doc/swift/swift72.htm 25 | */ 26 | public class UserHeaderBlock implements SwiftBlock { 27 | 28 | public static final String BLOCK_ID_3 = "3"; 29 | 30 | public final Optional bankingPriorityCode; 31 | 32 | public final String messageUserReference; 33 | 34 | public final ImmutableMap additionalSubblocks; 35 | 36 | 37 | public UserHeaderBlock(String bankingPriorityCode, String messageUserReference, Map additionalSubblocks) { 38 | 39 | Preconditions.checkArgument(messageUserReference != null, "messageUserReference can't be null"); 40 | Preconditions.checkArgument(additionalSubblocks != null, "additionalSubblocks can't be null"); 41 | 42 | this.bankingPriorityCode = Optional.ofNullable(bankingPriorityCode); 43 | this.messageUserReference = messageUserReference; 44 | this.additionalSubblocks = ImmutableMap.copyOf(additionalSubblocks); 45 | } 46 | 47 | public static UserHeaderBlock of(GeneralBlock block) throws BlockFieldParseException { 48 | Preconditions.checkArgument(block.getId().equals(BLOCK_ID_3), "unexpected block id '%s'", block.getId()); 49 | 50 | SwiftBlockReader blockReader = new SwiftBlockReader(new StringReader(block.getContent())); 51 | 52 | String bankingPriorityCode = null; 53 | String messageUserReference = null; 54 | Map additionalSubblocks = new HashMap<>(); 55 | 56 | try { 57 | GeneralBlock subblock; 58 | while ((subblock = blockReader.readBlock()) != null) { 59 | switch (subblock.getId()) { 60 | case "113": 61 | bankingPriorityCode = subblock.getContent(); // TODO regex check 62 | break; 63 | case "108": 64 | messageUserReference = subblock.getContent(); // TODO regex check 65 | break; 66 | default: 67 | additionalSubblocks.put(subblock.getId(), subblock); 68 | break; 69 | } 70 | } 71 | } catch (BlockParseException e) { 72 | throw new BlockFieldParseException("Block '" + block.getId() + "' content error", e); 73 | } 74 | 75 | return new UserHeaderBlock(bankingPriorityCode, messageUserReference, additionalSubblocks); 76 | } 77 | 78 | public Optional getBankingPriorityCode() { 79 | return bankingPriorityCode; 80 | } 81 | 82 | public String getMessageUserReference() { 83 | return messageUserReference; 84 | } 85 | 86 | @Override 87 | public String getId() { 88 | return BLOCK_ID_3; 89 | } 90 | 91 | @Override 92 | public String getContent() { 93 | StringBuilder contentBuilder = new StringBuilder(); 94 | if(bankingPriorityCode.isPresent()) { 95 | contentBuilder.append(BlockUtils.swiftTextOf("113", bankingPriorityCode.get())); 96 | } 97 | contentBuilder.append(BlockUtils.swiftTextOf("108", messageUserReference)); 98 | for (GeneralBlock subblock : additionalSubblocks.values()) { 99 | contentBuilder.append(BlockUtils.swiftTextOf(subblock.getId(), subblock.getContent())); 100 | } 101 | return contentBuilder.toString(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/UserTrailerBlock.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.qoomon.banking.swift.message.block.exception.BlockFieldParseException; 6 | import com.qoomon.banking.swift.message.block.exception.BlockParseException; 7 | 8 | import java.io.StringReader; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Optional; 12 | 13 | /** 14 | * User Header Block 15 | *

16 | * Sub Blocks 17 | *

 18 |  * 1: MAC - Message Authentication Code calculated based on the entire contents of the message using a key that has been exchanged with the destination and a secret algorithm. Found on message categories 1,2,4,5,7,8, most 6s and 304.
 19 |  * 2: PAC - Proprietary Authentication Code.
 20 |  * 3: CHK - Checksum calculated for all message types.
 21 |  * 4: TNG - Training.
 22 |  * 5: PDE - Possible Duplicate Emission added if user thinks the same message was sent previously
 23 |  * 6: DLM - Added by SWIFT if an urgent message (U) has not been delivered within 15 minutes, or a normal message (N) within 100 minutes.
 24 |  * 
25 | * Example
26 | * {MAC:12345678}{CHK:123456789ABC} 27 | * 28 | * @see https://www.ibm.com/support/knowledgecenter/SSBTEG_4.3.0/com.ibm.wbia_adapters.doc/doc/swift/swift72.htm 29 | */ 30 | public class UserTrailerBlock implements SwiftBlock { 31 | 32 | public static final String BLOCK_ID_5 = "5"; 33 | 34 | public final Optional messageAuthenticationCode; 35 | 36 | public final Optional proprietaryAuthenticationCode; 37 | 38 | public final Optional checksum; 39 | 40 | public final Optional training; 41 | 42 | public final Optional possibleDuplicateEmission; 43 | 44 | public final Optional deliveryDelay; 45 | 46 | private final ImmutableMap additionalSubblocks; 47 | 48 | 49 | public UserTrailerBlock(String messageAuthenticationCode, String proprietaryAuthenticationCode, String checksum, String training, String possibleDuplicateEmission, String deliveryDelay, Map additionalSubblocks) { 50 | 51 | Preconditions.checkArgument(additionalSubblocks != null, "additionalSubblocks can't be null"); 52 | 53 | this.messageAuthenticationCode = Optional.ofNullable(messageAuthenticationCode); 54 | this.proprietaryAuthenticationCode = Optional.ofNullable(proprietaryAuthenticationCode); 55 | this.checksum = Optional.ofNullable(checksum); 56 | this.training = Optional.ofNullable(training); 57 | this.possibleDuplicateEmission = Optional.ofNullable(possibleDuplicateEmission); 58 | this.deliveryDelay = Optional.ofNullable(deliveryDelay); 59 | this.additionalSubblocks = ImmutableMap.copyOf(additionalSubblocks); 60 | } 61 | 62 | public static UserTrailerBlock of(GeneralBlock block) throws BlockFieldParseException { 63 | Preconditions.checkArgument(block.getId().equals(BLOCK_ID_5), "unexpected block id 'v '", block.getId()); 64 | 65 | SwiftBlockReader blockReader = new SwiftBlockReader(new StringReader(block.getContent())); 66 | 67 | String messageAuthenticationCode = null; 68 | String proprietaryAuthenticationCode = null; 69 | String checksum = null; 70 | String training = null; 71 | String possibleDuplicateEmission = null; 72 | String deliveryDelay = null; 73 | Map additionalSubblocks = new HashMap<>(); 74 | 75 | try { 76 | GeneralBlock subblock; 77 | while ((subblock = blockReader.readBlock()) != null) { 78 | switch (subblock.getId()) { 79 | case "MAC": 80 | messageAuthenticationCode = subblock.getContent(); // TODO regex check 81 | break; 82 | case "PAC": 83 | proprietaryAuthenticationCode = subblock.getContent(); // TODO regex check 84 | break; 85 | case "CHK": 86 | checksum = subblock.getContent(); // TODO regex check 87 | break; 88 | case "TNG": 89 | training = subblock.getContent(); // TODO regex check 90 | break; 91 | case "PDE": 92 | possibleDuplicateEmission = subblock.getContent(); // TODO regex check 93 | break; 94 | case "DLM": 95 | deliveryDelay = subblock.getContent(); // TODO regex check 96 | break; 97 | default: 98 | additionalSubblocks.put(subblock.getId(), subblock); 99 | break; 100 | } 101 | } 102 | } catch (BlockParseException e) { 103 | throw new BlockFieldParseException("Block '" + block.getId() + "' content error", e); 104 | } 105 | 106 | return new UserTrailerBlock(messageAuthenticationCode, proprietaryAuthenticationCode, checksum, training, possibleDuplicateEmission, deliveryDelay, additionalSubblocks); 107 | } 108 | 109 | public Optional getMessageAuthenticationCode() { 110 | return messageAuthenticationCode; 111 | } 112 | 113 | public Optional getChecksum() { 114 | return checksum; 115 | } 116 | 117 | public Optional getPossibleDuplicateEmission() { 118 | return possibleDuplicateEmission; 119 | } 120 | 121 | public Optional getDeliveryDelay() { 122 | return deliveryDelay; 123 | } 124 | 125 | public GeneralBlock getAdditionalSubblock(String id) { 126 | return additionalSubblocks.get(id); 127 | } 128 | 129 | public Optional getTraining() { 130 | return training; 131 | } 132 | 133 | public Optional getProprietaryAuthenticationCode() { 134 | return proprietaryAuthenticationCode; 135 | } 136 | 137 | @Override 138 | public String getId() { 139 | return BLOCK_ID_5; 140 | } 141 | 142 | @Override 143 | public String getContent() { 144 | StringBuilder contentBuilder = new StringBuilder(); 145 | if(messageAuthenticationCode.isPresent()) { 146 | contentBuilder.append(BlockUtils.swiftTextOf("MAC", messageAuthenticationCode.get())); 147 | } 148 | if(proprietaryAuthenticationCode.isPresent()) { 149 | contentBuilder.append(BlockUtils.swiftTextOf("PAC", proprietaryAuthenticationCode.get())); 150 | } 151 | if(checksum.isPresent()) { 152 | contentBuilder.append(BlockUtils.swiftTextOf("CHK", checksum.get())); 153 | } 154 | if(training.isPresent()) { 155 | contentBuilder.append(BlockUtils.swiftTextOf("TNG", training.get())); 156 | } 157 | if(possibleDuplicateEmission.isPresent()) { 158 | contentBuilder.append(BlockUtils.swiftTextOf("PDE", possibleDuplicateEmission.get())); 159 | } 160 | if(deliveryDelay.isPresent()) { 161 | contentBuilder.append(BlockUtils.swiftTextOf("DLM", deliveryDelay.get())); 162 | } 163 | 164 | for (GeneralBlock subblock : additionalSubblocks.values()) { 165 | contentBuilder.append(BlockUtils.swiftTextOf(subblock.getId(), subblock.getContent())); 166 | } 167 | return contentBuilder.toString(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/exception/BlockFieldParseException.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block.exception; 2 | 3 | /** 4 | * Created by qoomon on 07/07/16. 5 | */ 6 | public class BlockFieldParseException extends Exception { 7 | 8 | 9 | public BlockFieldParseException(String message) { 10 | super(message); 11 | } 12 | 13 | 14 | public BlockFieldParseException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/block/exception/BlockParseException.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block.exception; 2 | 3 | /** 4 | * Created by qoomon on 07/07/16. 5 | */ 6 | public class BlockParseException extends Exception { 7 | 8 | private final int lineNumber; 9 | 10 | public BlockParseException(Throwable cause) { 11 | super(cause); 12 | this.lineNumber = 0; 13 | } 14 | 15 | public BlockParseException(String message) { 16 | super(message); 17 | this.lineNumber = 0; 18 | } 19 | 20 | public BlockParseException(String message, int lineNumber) { 21 | super(message + " at line " + lineNumber); 22 | this.lineNumber = lineNumber; 23 | } 24 | 25 | public BlockParseException(String message, int lineNumber, Throwable cause) { 26 | super(message + " at line " + lineNumber, cause); 27 | this.lineNumber = lineNumber; 28 | } 29 | 30 | public int getLineNumber() { 31 | return lineNumber; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/message/exception/SwiftMessageParseException.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.exception; 2 | 3 | import com.qoomon.banking.swift.message.block.exception.BlockParseException; 4 | 5 | /** 6 | * Created by qoomon on 27/06/16. 7 | */ 8 | public class SwiftMessageParseException extends Exception { 9 | 10 | private final int lineNumber; 11 | 12 | public SwiftMessageParseException(String message, int lineNumber) { 13 | super(message + " at line number " + lineNumber); 14 | this.lineNumber = lineNumber; 15 | } 16 | 17 | public SwiftMessageParseException(String message, int lineNumber, Throwable cause) { 18 | super(message + " at line number " + lineNumber, cause); 19 | this.lineNumber = lineNumber; 20 | } 21 | 22 | public SwiftMessageParseException(int lineNumber, Throwable cause) { 23 | super("at line number " + lineNumber, cause); 24 | this.lineNumber = lineNumber; 25 | } 26 | 27 | public SwiftMessageParseException(BlockParseException e) { 28 | super(e); 29 | lineNumber = 0; 30 | } 31 | 32 | public int getLineNumber() { 33 | return lineNumber; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/notation/FieldNotation.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.notation; 2 | 3 | import com.google.common.base.Preconditions; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * Created by qoomon on 29/07/16. 9 | */ 10 | public class FieldNotation { 11 | 12 | public static final String FIXED_LENGTH_SIGN = "!"; 13 | public static final String RANGE_LENGTH_SIGN = "-"; 14 | public static final String MULTILINE_LENGTH_SIGN = "*"; 15 | 16 | private final Boolean optional; 17 | private final Optional prefix; 18 | private final String charSet; 19 | private final Integer length0; 20 | private final Optional length1; 21 | private final Optional lengthSign; 22 | 23 | public FieldNotation(Boolean optional, String prefix, String charSet, Integer length0, Integer length1, String lengthSign) { 24 | 25 | Preconditions.checkArgument(optional != null, "optional can't be null"); 26 | Preconditions.checkArgument(charSet != null, "charSet can't be null"); 27 | Preconditions.checkArgument(length0 != null, "length0 can't be null"); 28 | 29 | this.optional = optional; 30 | this.prefix = Optional.ofNullable(prefix); 31 | this.charSet = charSet; 32 | this.length0 = length0; 33 | this.length1 = Optional.ofNullable(length1); 34 | this.lengthSign = Optional.ofNullable(lengthSign); 35 | 36 | if (!this.lengthSign.isPresent()) { 37 | Preconditions.checkArgument(!this.length1.isPresent(), "Missing field length sign between field lengths : '%s'", this); 38 | } else switch (this.lengthSign.get()) { 39 | case FIXED_LENGTH_SIGN: 40 | Preconditions.checkArgument(!this.length1.isPresent(), "Unexpected field length after fixed length sign %s : '%s'", FIXED_LENGTH_SIGN, this); 41 | break; 42 | case RANGE_LENGTH_SIGN: 43 | Preconditions.checkArgument(this.length1.isPresent(), "Missing field length after range length sign %s : '%s'", RANGE_LENGTH_SIGN, this); 44 | break; 45 | case MULTILINE_LENGTH_SIGN: 46 | Preconditions.checkArgument(this.length1.isPresent(), "Missing field length after multiline length sign %s : '%s'", MULTILINE_LENGTH_SIGN, this); 47 | break; 48 | default: 49 | Preconditions.checkArgument(false, "Unknown length sign : '" + this.toString() + "'"); 50 | break; 51 | } 52 | } 53 | 54 | public Boolean isOptional() { 55 | return optional; 56 | } 57 | 58 | public Integer getLength0() { 59 | return length0; 60 | } 61 | 62 | public Optional getLength1() { 63 | return length1; 64 | } 65 | 66 | public Optional getLengthSign() { 67 | return lengthSign; 68 | } 69 | 70 | public String getCharSet() { 71 | return charSet; 72 | } 73 | 74 | public Optional getPrefix() { 75 | return prefix; 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | String fieldNotation = ""; 81 | 82 | if (prefix.isPresent()) { 83 | fieldNotation += prefix.get(); 84 | } 85 | 86 | fieldNotation += length0; 87 | if (lengthSign.isPresent()) { 88 | fieldNotation += lengthSign.get(); 89 | if (lengthSign.get().equals(RANGE_LENGTH_SIGN) || lengthSign.get().equals(MULTILINE_LENGTH_SIGN)) { 90 | fieldNotation += length1.get(); 91 | } 92 | } 93 | fieldNotation += charSet; 94 | if (optional) { 95 | fieldNotation = "[" + fieldNotation + "]"; 96 | } 97 | return fieldNotation; 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/notation/FieldNotationParseException.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.notation; 2 | 3 | /** 4 | * Created by qoomon on 27/06/16. 5 | */ 6 | public class FieldNotationParseException extends Exception { 7 | 8 | private final int index; 9 | 10 | public FieldNotationParseException(String message, int index) { 11 | super(message + " at index " + index); 12 | this.index = index; 13 | } 14 | 15 | public int getIndex() { 16 | return index; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/notation/SwiftDecimalFormatter.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.notation; 2 | 3 | import java.math.BigDecimal; 4 | import java.text.DecimalFormat; 5 | import java.text.DecimalFormatSymbols; 6 | import java.text.ParseException; 7 | 8 | /** 9 | * Created by qoomon on 21/07/16. 10 | */ 11 | public class SwiftDecimalFormatter { 12 | 13 | private final static DecimalFormat DECIMAL_FORMAT = new DecimalFormat(); 14 | 15 | static { 16 | DecimalFormatSymbols decimalFormatSymbols = new DecimalFormatSymbols(); 17 | decimalFormatSymbols.setDecimalSeparator(','); 18 | 19 | DECIMAL_FORMAT.setDecimalSeparatorAlwaysShown(true); 20 | DECIMAL_FORMAT.setGroupingUsed(false); 21 | DECIMAL_FORMAT.setMinimumIntegerDigits(1); 22 | DECIMAL_FORMAT.setMinimumFractionDigits(0); 23 | DECIMAL_FORMAT.setDecimalFormatSymbols(decimalFormatSymbols); 24 | DECIMAL_FORMAT.setMaximumFractionDigits(Integer.MAX_VALUE); 25 | DECIMAL_FORMAT.setParseBigDecimal(true); 26 | } 27 | 28 | public static BigDecimal parse(String numberText) { 29 | try { 30 | return (BigDecimal) DECIMAL_FORMAT.parse(numberText); 31 | } catch (ParseException e) { 32 | throw new IllegalArgumentException(e); 33 | } 34 | } 35 | 36 | public static String format(BigDecimal number) { 37 | return DECIMAL_FORMAT.format(number); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/notation/SwiftNotationParseException.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.notation; 2 | 3 | /** 4 | * Created by qoomon on 03/08/16. 5 | */ 6 | public class SwiftNotationParseException extends RuntimeException { 7 | 8 | public SwiftNotationParseException(String message) { 9 | super(message); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/Page.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage; 2 | 3 | public interface Page { 4 | 5 | String getId(); 6 | 7 | String getContent(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/PageReader.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage; 2 | 3 | import com.qoomon.banking.swift.message.exception.SwiftMessageParseException; 4 | import com.qoomon.banking.swift.submessage.exception.PageParserException; 5 | import com.qoomon.banking.swift.submessage.field.GeneralField; 6 | import com.qoomon.banking.swift.submessage.field.SwiftFieldReader; 7 | 8 | import java.util.LinkedList; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | import static java.lang.String.join; 13 | 14 | public abstract class PageReader { 15 | 16 | public final List readAll() throws SwiftMessageParseException { 17 | List result = new LinkedList<>(); 18 | T page; 19 | while ((page = read()) != null) { 20 | result.add(page); 21 | } 22 | return result; 23 | } 24 | 25 | public abstract T read() throws SwiftMessageParseException; 26 | 27 | public static void ensureValidField(GeneralField field, Set expectedFieldTagSet, SwiftFieldReader fieldReader) { 28 | if (field == null) { 29 | throw new PageParserException("Expected field(s): " + join(", ", expectedFieldTagSet) + "," + 30 | " but was end of file", fieldReader.getFieldLineNumber()); 31 | } 32 | if (!expectedFieldTagSet.contains(field.getTag())) { 33 | throw new PageParserException("Expected field(s): " + join(", ", expectedFieldTagSet) + "," + 34 | " but was '" + field.getTag() + "'", fieldReader.getFieldLineNumber()); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/PageSeparator.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage; 2 | 3 | /** 4 | * Created by qoomon on 08/07/2016. 5 | */ 6 | public final class PageSeparator { 7 | 8 | private PageSeparator() {} 9 | 10 | public static final String TAG = "-"; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/exception/PageParserException.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.exception; 2 | 3 | /** 4 | * Created by qoomon on 27/06/16. 5 | */ 6 | public class PageParserException extends RuntimeException { 7 | 8 | private final int lineNumber; 9 | 10 | public PageParserException(String message, int lineNumber) { 11 | super(message + " at line number " + lineNumber); 12 | this.lineNumber = lineNumber; 13 | } 14 | 15 | public PageParserException(String message, int lineNumber, Throwable cause) { 16 | super(message + " at line number " + lineNumber, cause); 17 | this.lineNumber = lineNumber; 18 | } 19 | 20 | public PageParserException(Throwable cause) { 21 | super(cause); 22 | this.lineNumber = 0; 23 | } 24 | 25 | public int getLineNumber() { 26 | return lineNumber; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/AccountIdentification.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftNotation; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Account Identification 12 | *

13 | * Field Tag :25: 14 | *

15 | * Format 35x 16 | *

17 | * SubFields 18 | *

{@literal
19 |  * 1: 35x - Value
20 |  * }
21 | */ 22 | public class AccountIdentification implements SwiftField { 23 | 24 | public static final String FIELD_TAG_25 = "25"; 25 | 26 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("35x"); 27 | 28 | private final String content; 29 | 30 | 31 | public AccountIdentification(String content) { 32 | Preconditions.checkArgument(content != null, "content can't be null"); 33 | this.content = content; 34 | } 35 | 36 | public static AccountIdentification of(GeneralField field) throws FieldNotationParseException { 37 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_25), "unexpected field tag '%s'", field.getTag()); 38 | 39 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 40 | 41 | String value = subFields.get(0); 42 | 43 | return new AccountIdentification(value); 44 | } 45 | 46 | 47 | @Override 48 | public String getTag() { 49 | return FIELD_TAG_25; 50 | } 51 | 52 | @Override 53 | public String getContent() { 54 | try { 55 | return SWIFT_NOTATION.render(Lists.newArrayList(content)); 56 | } catch (FieldNotationParseException e) { 57 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/ClosingAvailableBalance.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftDecimalFormatter; 7 | import com.qoomon.banking.swift.notation.SwiftNotation; 8 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 9 | import org.joda.money.BigMoney; 10 | import org.joda.money.CurrencyUnit; 11 | 12 | import java.math.BigDecimal; 13 | import java.time.LocalDate; 14 | import java.time.format.DateTimeFormatter; 15 | import java.util.List; 16 | 17 | /** 18 | * Closing Available Balance (Available Funds) 19 | *

20 | * Field Tag :64: 21 | *

22 | * Format 1!a6!n3!a15d 23 | *

24 | * SubFields 25 | *

 26 |  * 1: 1!a - Debit/Credit Mark - 'D' = Debit, 'C' Credit
 27 |  * 2: 6!n - Entry Date - Format 'YYMMDD'
 28 |  * 3: 3!a - Currency - Three Digit Code
 29 |  * 4: 15d - Amount
 30 |  * 
31 | */ 32 | public class ClosingAvailableBalance implements SwiftField { 33 | 34 | public static final String FIELD_TAG_64 = "64"; 35 | 36 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("1!a6!n3!a15d"); 37 | 38 | private static final DateTimeFormatter ENTRY_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd"); 39 | 40 | private final DebitCreditMark debitCreditMark; 41 | 42 | private final LocalDate entryDate; 43 | 44 | private final BigMoney amount; 45 | 46 | 47 | public ClosingAvailableBalance(LocalDate entryDate, DebitCreditMark debitCreditMark, BigMoney amount) { 48 | 49 | Preconditions.checkArgument(debitCreditMark != null, "debitCreditMark can't be null"); 50 | Preconditions.checkArgument(entryDate != null, "entryDate can't be null"); 51 | Preconditions.checkArgument(amount != null, "amount can't be null"); 52 | Preconditions.checkArgument(amount.isPositiveOrZero(), "amount can't be negative"); 53 | 54 | this.debitCreditMark = debitCreditMark; 55 | this.entryDate = entryDate; 56 | this.amount = amount; 57 | } 58 | 59 | public static ClosingAvailableBalance of(GeneralField field) throws FieldNotationParseException { 60 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_64), "unexpected field tag '%s'", field.getTag()); 61 | 62 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 63 | 64 | DebitCreditMark debitCreditMark = DebitCreditMark.ofFieldValue(subFields.get(0)); 65 | LocalDate entryDate = LocalDate.parse(subFields.get(1), ENTRY_DATE_FORMATTER); 66 | CurrencyUnit amountCurrency = CurrencyUnit.of(subFields.get(2)); 67 | BigDecimal amountValue = SwiftDecimalFormatter.parse(subFields.get(3)); 68 | BigMoney amount = BigMoney.of(amountCurrency, amountValue); 69 | 70 | return new ClosingAvailableBalance(entryDate, debitCreditMark, amount); 71 | } 72 | 73 | public DebitCreditMark getDebitCreditMark() { 74 | return debitCreditMark; 75 | } 76 | 77 | public LocalDate getEntryDate() { 78 | return entryDate; 79 | } 80 | 81 | public BigMoney getAmount() { 82 | return amount; 83 | } 84 | 85 | public BigMoney getSignedAmount() { 86 | if (getDebitCreditMark().sign() < 0) { 87 | return amount.negated(); 88 | } 89 | return amount; 90 | } 91 | 92 | @Override 93 | public String getTag() { 94 | return FIELD_TAG_64; 95 | } 96 | 97 | @Override 98 | public String getContent() { 99 | try { 100 | return SWIFT_NOTATION.render(Lists.newArrayList( 101 | debitCreditMark.toFieldValue(), 102 | ENTRY_DATE_FORMATTER.format(entryDate), 103 | amount.getCurrencyUnit().getCode(), 104 | SwiftDecimalFormatter.format(amount.getAmount()) 105 | )); 106 | } catch (FieldNotationParseException e) { 107 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/ClosingBalance.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftDecimalFormatter; 7 | import com.qoomon.banking.swift.notation.SwiftNotation; 8 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 9 | import org.joda.money.BigMoney; 10 | import org.joda.money.CurrencyUnit; 11 | 12 | import java.math.BigDecimal; 13 | import java.time.LocalDate; 14 | import java.time.format.DateTimeFormatter; 15 | import java.util.List; 16 | 17 | /** 18 | * Closing Balance (Booked Funds) 19 | *

20 | * Field Tag :62F: 21 | * Field Tag :62M: - Intermediate Balance 22 | *

23 | * Format 1!a6!n3!a15d 24 | *

25 | * SubFields 26 | *

 27 |  * 1: 1!a - Debit/Credit Mark - 'D' = Debit, 'C' Credit
 28 |  * 2: 6!n - Date - Format 'YYMMDD'
 29 |  * 3: 6!n - Currency - Three Digit Code
 30 |  * 4: 15d - Amount
 31 |  * 
32 | */ 33 | public class ClosingBalance implements SwiftField { 34 | 35 | public static final String FIELD_TAG_62F = "62F"; 36 | public static final String FIELD_TAG_62M = "62M"; 37 | 38 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("1!a6!n3!a15d"); 39 | 40 | private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd"); 41 | 42 | private final Type type; 43 | 44 | private final DebitCreditMark debitCreditMark; 45 | 46 | private final LocalDate date; 47 | 48 | private final BigMoney amount; 49 | 50 | 51 | public ClosingBalance(Type type, LocalDate date, DebitCreditMark debitCreditMark, BigMoney amount) { 52 | 53 | Preconditions.checkArgument(type != null, "type can't be null"); 54 | Preconditions.checkArgument(debitCreditMark != null, "debitCreditMark can't be null"); 55 | Preconditions.checkArgument(date != null, "date can't be null"); 56 | Preconditions.checkArgument(amount != null, "amount can't be null"); 57 | Preconditions.checkArgument(amount.isPositiveOrZero(), "amount can't be negative"); 58 | 59 | this.type = type; 60 | this.debitCreditMark = debitCreditMark; 61 | this.date = date; 62 | this.amount = amount; 63 | } 64 | 65 | public static ClosingBalance of(GeneralField field) throws FieldNotationParseException { 66 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_62F) || field.getTag().equals(FIELD_TAG_62M), "unexpected field tag '%s'", field.getTag()); 67 | Type type = field.getTag().equals(FIELD_TAG_62F) ? Type.CLOSING : Type.INTERMEDIATE; 68 | 69 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 70 | 71 | DebitCreditMark debitCreditMark = DebitCreditMark.ofFieldValue(subFields.get(0)); 72 | LocalDate date = LocalDate.parse(subFields.get(1), DATE_FORMATTER); 73 | CurrencyUnit amountCurrency = CurrencyUnit.of(subFields.get(2)); 74 | BigDecimal amountValue = SwiftDecimalFormatter.parse(subFields.get(3)); 75 | BigMoney amount = BigMoney.of(amountCurrency, amountValue); 76 | 77 | return new ClosingBalance(type, date, debitCreditMark, amount); 78 | } 79 | 80 | public Type getType() { 81 | return type; 82 | } 83 | 84 | public DebitCreditMark getDebitCreditMark() { 85 | return debitCreditMark; 86 | } 87 | 88 | public LocalDate getDate() { 89 | return date; 90 | } 91 | 92 | public BigMoney getAmount() { 93 | return amount; 94 | } 95 | 96 | public BigMoney getSignedAmount() { 97 | if(getDebitCreditMark().sign() < 0) { 98 | return amount.negated(); 99 | } 100 | return amount; 101 | } 102 | 103 | @Override 104 | public String getTag() { 105 | return type == Type.CLOSING ? FIELD_TAG_62F : FIELD_TAG_62M; 106 | } 107 | 108 | @Override 109 | public String getContent() { 110 | try { 111 | return SWIFT_NOTATION.render(Lists.newArrayList( 112 | debitCreditMark.toFieldValue(), 113 | DATE_FORMATTER.format(date), 114 | amount.getCurrencyUnit().getCode(), 115 | SwiftDecimalFormatter.format(amount.getAmount()))); 116 | } catch (FieldNotationParseException e) { 117 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 118 | } 119 | } 120 | 121 | public enum Type { 122 | CLOSING, 123 | INTERMEDIATE 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/DateTimeIndicator.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Joiner; 4 | import com.google.common.base.Preconditions; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftNotation; 7 | 8 | import java.time.OffsetDateTime; 9 | import java.time.format.DateTimeFormatter; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | /** 14 | * Date Time Indicator 15 | *

16 | * Field Tag :13D: 17 | *

18 | * Format 6!n4!n1x4!n 19 | *

20 | * SubFields 21 | *

22 |  * 1: 6!n - Date - Format 'YYMMDD'
23 |  * 2: 4!n - Time - Format 'hhmm'
24 |  * 3: 1x  - Offset sign - '+' or '-'
25 |  * 4: 4!n - Offset - Format 'hhmm'
26 |  * 
27 | * Example 28 | *
29 |  * 1605191047+0100
30 |  * 
31 | */ 32 | public class DateTimeIndicator implements SwiftField { 33 | 34 | public static final String FIELD_TAG_13D = "13D"; 35 | 36 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("6!n4!n1x4!n"); 37 | 38 | private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyMMddHHmmZ"); 39 | 40 | private final OffsetDateTime dateTime; 41 | 42 | 43 | public DateTimeIndicator(OffsetDateTime dateTime) { 44 | 45 | Preconditions.checkArgument(dateTime != null, "dateTime can't be null"); 46 | 47 | this.dateTime = dateTime; 48 | } 49 | 50 | public static DateTimeIndicator of(GeneralField field) throws FieldNotationParseException { 51 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_13D), "unexpected field tag '%s'", field.getTag()); 52 | 53 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 54 | 55 | OffsetDateTime value = OffsetDateTime.parse(Joiner.on("").join(subFields), DATE_TIME_FORMATTER); 56 | 57 | return new DateTimeIndicator(value); 58 | } 59 | 60 | public OffsetDateTime getDateTime() { 61 | return dateTime; 62 | } 63 | 64 | @Override 65 | public String getTag() { 66 | return FIELD_TAG_13D; 67 | } 68 | 69 | @Override 70 | public String getContent() { 71 | try { 72 | return SWIFT_NOTATION.render(SWIFT_NOTATION.parse(DATE_TIME_FORMATTER.format(dateTime))); 73 | } catch (FieldNotationParseException e) { 74 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/FieldUtils.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Joiner; 4 | import com.google.common.base.Splitter; 5 | 6 | /** 7 | * Created by qoomon on 15/08/16. 8 | */ 9 | public final class FieldUtils { 10 | 11 | /** 12 | * Separates a string. 13 | * 14 | * @param splitter splitter 15 | * @param joiner joiner 16 | * @param text text to separate. 17 | * @return separated text 18 | */ 19 | public static String seperate(final String text, final Splitter splitter, final Joiner joiner) { 20 | return joiner.join(splitter.split(text)); 21 | } 22 | 23 | /** 24 | * Separates a string. 25 | * 26 | * @param maxLineLength max line length 27 | * @param text text to separate. 28 | * @return separated text 29 | */ 30 | public static String breakIntoLines(final String text, final int maxLineLength) { 31 | return seperate(text, 32 | Splitter.fixedLength(maxLineLength), 33 | Joiner.on("\n") 34 | ); 35 | } 36 | 37 | /** 38 | * Convert to Swift Text Format. 39 | * 40 | * @param field to convert 41 | * @return swift text 42 | */ 43 | public static String swiftTextOf(SwiftField field) { 44 | return ":" + field.getTag() + ":" + field.getContent(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/FloorLimitIndicator.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftDecimalFormatter; 7 | import com.qoomon.banking.swift.notation.SwiftNotation; 8 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 9 | import org.joda.money.BigMoney; 10 | import org.joda.money.CurrencyUnit; 11 | 12 | import java.math.BigDecimal; 13 | import java.util.List; 14 | import java.util.Objects; 15 | import java.util.Optional; 16 | 17 | /** 18 | * Floor Limit Indicator Debit/Credit 19 | *

20 | * Field Tag :34F: 21 | *

22 | * Format 3!a[1!a]15d 23 | *

24 | * SubFields 25 | *

 26 |  * 1: 3!a   - Currency - Three Digit Code
 27 |  * 2: [1!a] - Debit/Credit Mark - 'D' = Debit, 'C' Credit
 28 |  * 3: 15d   - Amount
 29 |  * 
30 | */ 31 | public class FloorLimitIndicator implements SwiftField { 32 | 33 | public static final String FIELD_TAG_34F = "34F"; 34 | 35 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("3!a[1!a]15d"); 36 | 37 | private final Optional debitCreditMark; 38 | 39 | private final BigMoney amount; 40 | 41 | 42 | public FloorLimitIndicator(DebitCreditMark debitCreditMark, BigMoney amount) { 43 | 44 | Preconditions.checkArgument(amount != null, "amount can't be null"); 45 | Preconditions.checkArgument(amount.isPositiveOrZero(), "amount can't be negative"); 46 | 47 | this.debitCreditMark = Optional.ofNullable(debitCreditMark); 48 | this.amount = amount; 49 | } 50 | 51 | public static FloorLimitIndicator of(GeneralField field) throws FieldNotationParseException { 52 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_34F), "unexpected field tag '%s'", field.getTag()); 53 | 54 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 55 | 56 | CurrencyUnit amountCurrency = CurrencyUnit.of(subFields.get(0)); 57 | DebitCreditMark debitCreditMark = subFields.get(1) != null ? DebitCreditMark.ofFieldValue(subFields.get(1)) : null; 58 | BigDecimal amountValue = SwiftDecimalFormatter.parse(subFields.get(2)); 59 | BigMoney amount = BigMoney.of(amountCurrency, amountValue); 60 | 61 | return new FloorLimitIndicator(debitCreditMark, amount); 62 | } 63 | 64 | 65 | public Optional getDebitCreditMark() { 66 | return debitCreditMark; 67 | } 68 | 69 | public BigMoney getAmount() { 70 | return amount; 71 | } 72 | 73 | public Optional getSignedAmount() { 74 | return getDebitCreditMark().map( 75 | debitCreditMark -> { 76 | if (debitCreditMark.sign() < 0) { 77 | return amount.negated(); 78 | } 79 | return amount; 80 | }); 81 | } 82 | 83 | @Override 84 | public String getTag() { 85 | return FIELD_TAG_34F; 86 | } 87 | 88 | @Override 89 | public String getContent() { 90 | try { 91 | return SWIFT_NOTATION.render(Lists.newArrayList( 92 | amount.getCurrencyUnit().getCode(), 93 | debitCreditMark.map(DebitCreditMark::toFieldValue).orElse(null), 94 | SwiftDecimalFormatter.format(amount.getAmount()) 95 | )); 96 | } catch (FieldNotationParseException e) { 97 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/ForwardAvailableBalance.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftDecimalFormatter; 7 | import com.qoomon.banking.swift.notation.SwiftNotation; 8 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 9 | import org.joda.money.BigMoney; 10 | import org.joda.money.CurrencyUnit; 11 | 12 | import java.math.BigDecimal; 13 | import java.time.LocalDate; 14 | import java.time.format.DateTimeFormatter; 15 | import java.util.List; 16 | 17 | /** 18 | * Forward Available Balance 19 | *

20 | * Field Tag :65: 21 | *

22 | * Format 1!a6!n3!a15d 23 | *

24 | * SubFields 25 | *

 26 |  * 1: 1!a - Debit/Credit Mark - 'D' = Debit, 'C' Credit
 27 |  * 2: 6!n - Entry date - Format 'YYMMDD'
 28 |  * 3: 3!a - Currency - Three Digit Code
 29 |  * 4: 15d - Amount
 30 |  * 
31 | */ 32 | public class ForwardAvailableBalance implements SwiftField { 33 | 34 | public static final String FIELD_TAG_65 = "65"; 35 | 36 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("1!a6!n3!a15d"); 37 | 38 | private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd"); 39 | 40 | private final DebitCreditMark debitCreditMark; 41 | 42 | private final LocalDate entryDate; 43 | 44 | private final BigMoney amount; 45 | 46 | 47 | public ForwardAvailableBalance( LocalDate entryDate, DebitCreditMark debitCreditMark, BigMoney amount) { 48 | 49 | Preconditions.checkArgument(debitCreditMark != null, "debitCreditMark can't be null"); 50 | Preconditions.checkArgument(entryDate != null, "entryDate can't be null"); 51 | Preconditions.checkArgument(amount != null, "amount can't be null"); 52 | Preconditions.checkArgument(amount.isPositiveOrZero(), "amount can't be negative"); 53 | 54 | this.debitCreditMark = debitCreditMark; 55 | this.entryDate = entryDate; 56 | this.amount = amount; 57 | } 58 | 59 | public static ForwardAvailableBalance of(GeneralField field) throws FieldNotationParseException { 60 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_65), "unexpected field tag '%s'", field.getTag()); 61 | 62 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 63 | 64 | DebitCreditMark debitCreditMark = DebitCreditMark.ofFieldValue(subFields.get(0)); 65 | LocalDate entryDate = LocalDate.parse(subFields.get(1), DATE_FORMATTER); 66 | CurrencyUnit amountCurrency = CurrencyUnit.of(subFields.get(2)); 67 | BigDecimal amountValue = SwiftDecimalFormatter.parse(subFields.get(3)); 68 | 69 | BigMoney amount = BigMoney.of(amountCurrency, amountValue); 70 | 71 | return new ForwardAvailableBalance(entryDate, debitCreditMark, amount); 72 | } 73 | 74 | public DebitCreditMark getDebitCreditMark() { 75 | return debitCreditMark; 76 | } 77 | 78 | public LocalDate getEntryDate() { 79 | return entryDate; 80 | } 81 | 82 | public BigMoney getAmount() { 83 | return amount; 84 | } 85 | 86 | public BigMoney getSignedAmount() { 87 | if(getDebitCreditMark().sign() < 0) { 88 | return amount.negated(); 89 | } 90 | return amount; 91 | } 92 | 93 | @Override 94 | public String getTag() { 95 | return FIELD_TAG_65; 96 | } 97 | 98 | @Override 99 | public String getContent() { 100 | try { 101 | return SWIFT_NOTATION.render(Lists.newArrayList( 102 | 103 | debitCreditMark.toFieldValue(), 104 | DATE_FORMATTER.format(entryDate), 105 | amount.getCurrencyUnit().getCode(), 106 | SwiftDecimalFormatter.format(amount.getAmount()) 107 | )); 108 | } catch (FieldNotationParseException e) { 109 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/GeneralField.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | 5 | /** 6 | * Created by qoomon on 27/06/16. 7 | */ 8 | public class GeneralField implements SwiftField { 9 | 10 | private final String tag; 11 | 12 | private final String content; 13 | 14 | 15 | public GeneralField(String tag, String content) { 16 | 17 | Preconditions.checkArgument(tag != null && !tag.isEmpty(), "tag can't be null or empty"); 18 | Preconditions.checkArgument(content != null, "content can't be null"); 19 | 20 | this.tag = tag; 21 | this.content = content; 22 | } 23 | 24 | @Override 25 | public String getTag() { 26 | return tag; 27 | } 28 | 29 | @Override 30 | public String getContent() { 31 | return content; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/InformationToAccountOwner.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftNotation; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Information to Account Owner 12 | *

13 | * Field Tag :86: 14 | *

15 | * Format 6*65x 16 | *

17 | * SubFields 18 | *

19 |  * 1: 6*65x - Value
20 |  * 
21 | */ 22 | public class InformationToAccountOwner implements SwiftField { 23 | 24 | public static final String FIELD_TAG_86 = "86"; 25 | 26 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("6*65x"); 27 | 28 | private final String content; 29 | 30 | 31 | public InformationToAccountOwner(String content) { 32 | 33 | Preconditions.checkArgument(content != null, "content can't be null"); 34 | 35 | this.content = content; 36 | } 37 | 38 | public static InformationToAccountOwner of(GeneralField field) throws FieldNotationParseException { 39 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_86), "unexpected field tag '%s'", field.getTag()); 40 | 41 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 42 | 43 | String value = subFields.get(0); 44 | 45 | return new InformationToAccountOwner(value); 46 | } 47 | 48 | @Override 49 | public String getTag() { 50 | return FIELD_TAG_86; 51 | } 52 | 53 | @Override 54 | public String getContent() { 55 | try { 56 | return SWIFT_NOTATION.render(Lists.newArrayList(content)); 57 | } catch (FieldNotationParseException e) { 58 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/OpeningBalance.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftDecimalFormatter; 7 | import com.qoomon.banking.swift.notation.SwiftNotation; 8 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 9 | import org.joda.money.BigMoney; 10 | import org.joda.money.CurrencyUnit; 11 | 12 | import java.math.BigDecimal; 13 | import java.time.LocalDate; 14 | import java.time.format.DateTimeFormatter; 15 | import java.util.List; 16 | 17 | /** 18 | * Opening Balance 19 | *

20 | * Field Tag :60F: 21 | * Field Tag :60M: - Intermediate Balance 22 | *

23 | * Format 1!a6!n3!a15d 24 | *

25 | * SubFields 26 | *

 27 |  * 1: 1!a - Debit/Credit Mark - 'D' = Debit, 'C' Credit
 28 |  * 2: 6!n - Date - Format 'YYMMDD'
 29 |  * 3: 3!a - Currency - Three Digit Code
 30 |  * 4: 15d - Amount
 31 |  * 
32 | */ 33 | public class OpeningBalance implements SwiftField { 34 | 35 | public static final String FIELD_TAG_60F = "60F"; 36 | public static final String FIELD_TAG_60M = "60M"; 37 | 38 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("1!a6!n3!a15d"); 39 | 40 | private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd"); 41 | 42 | private final Type type; 43 | 44 | private final DebitCreditMark debitCreditMark; 45 | 46 | private final LocalDate date; 47 | 48 | private final BigMoney amount; 49 | 50 | 51 | public OpeningBalance(Type type, LocalDate date, DebitCreditMark debitCreditMark, BigMoney amount) { 52 | 53 | Preconditions.checkArgument(type != null, "type can't be null"); 54 | Preconditions.checkArgument(debitCreditMark != null, "debitCreditMark can't be null"); 55 | Preconditions.checkArgument(date != null, "date can't be null"); 56 | Preconditions.checkArgument(amount != null, "amount can't be null"); 57 | Preconditions.checkArgument(amount.isPositiveOrZero(), "amount can't be negative"); 58 | 59 | this.type = type; 60 | this.debitCreditMark = debitCreditMark; 61 | this.date = date; 62 | this.amount = amount; 63 | } 64 | 65 | public static OpeningBalance of(GeneralField field) throws FieldNotationParseException { 66 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_60F) || field.getTag().equals(FIELD_TAG_60M), "unexpected field tag '%s'", field.getTag()); 67 | Type type = field.getTag().equals(FIELD_TAG_60F) ? Type.OPENING : Type.INTERMEDIATE; 68 | 69 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 70 | 71 | DebitCreditMark debitCreditMark = subFields.get(0) != null ? DebitCreditMark.ofFieldValue(subFields.get(0)) : null; 72 | LocalDate date = LocalDate.parse(subFields.get(1), DATE_FORMATTER); 73 | CurrencyUnit amountCurrency = CurrencyUnit.of(subFields.get(2)); 74 | BigDecimal amountValue = SwiftDecimalFormatter.parse(subFields.get(3)); 75 | BigMoney amount = BigMoney.of(amountCurrency, amountValue); 76 | 77 | return new OpeningBalance(type, date, debitCreditMark, amount); 78 | } 79 | 80 | public Type getType() { 81 | return type; 82 | } 83 | 84 | public DebitCreditMark getDebitCreditMark() { 85 | return debitCreditMark; 86 | } 87 | 88 | public LocalDate getDate() { 89 | return date; 90 | } 91 | 92 | public BigMoney getAmount() { 93 | return amount; 94 | } 95 | 96 | public BigMoney getSignedAmount() { 97 | if (getDebitCreditMark().sign() < 0) { 98 | return amount.negated(); 99 | } 100 | return amount; 101 | } 102 | 103 | @Override 104 | public String getTag() { 105 | return type == Type.OPENING ? FIELD_TAG_60F : FIELD_TAG_60M; 106 | } 107 | 108 | @Override 109 | public String getContent() { 110 | try { 111 | return SWIFT_NOTATION.render(Lists.newArrayList( 112 | debitCreditMark.toFieldValue(), 113 | DATE_FORMATTER.format(date), 114 | amount.getCurrencyUnit().getCode(), 115 | SwiftDecimalFormatter.format(amount.getAmount()) 116 | )); 117 | } catch (FieldNotationParseException e) { 118 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 119 | } 120 | } 121 | 122 | public enum Type { 123 | OPENING, 124 | INTERMEDIATE 125 | } 126 | 127 | 128 | } -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/RelatedReference.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftNotation; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Related Reference 12 | *

13 | * Field Tag :21: 14 | *

15 | * Format 16x 16 | *

17 | * SubFields 18 | *

19 |  * 1: 16x - Value
20 |  * 
21 | */ 22 | public class RelatedReference implements SwiftField { 23 | 24 | public static final String FIELD_TAG_21 = "21"; 25 | 26 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("16x"); 27 | 28 | private final String content; 29 | 30 | 31 | public RelatedReference(String content) { 32 | 33 | Preconditions.checkArgument(content != null, "content can't be null"); 34 | 35 | this.content = content; 36 | } 37 | 38 | public static RelatedReference of(GeneralField field) throws FieldNotationParseException { 39 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_21), "unexpected field tag '%s'", field.getTag()); 40 | 41 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 42 | 43 | String value = subFields.get(0); 44 | 45 | return new RelatedReference(value); 46 | } 47 | 48 | 49 | 50 | @Override 51 | public String getTag() { 52 | return FIELD_TAG_21; 53 | } 54 | 55 | @Override 56 | public String getContent() { 57 | try { 58 | return SWIFT_NOTATION.render(Lists.newArrayList(content)); 59 | } catch (FieldNotationParseException e) { 60 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/StatementNumber.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftNotation; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | /** 12 | * Statement Number 13 | *

14 | * Field Tag :28C: 15 | *

16 | * Format 5n[/5n] 17 | *

18 | * SubFields 19 | *

20 |  * 1: 5n    - Statement Number
21 |  * 2: [/5n] - Sequence Number
22 |  * 
23 | */ 24 | public class StatementNumber implements SwiftField { 25 | 26 | public static final String FIELD_TAG_28C = "28C"; 27 | 28 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("5n[/5n]"); 29 | 30 | private final String statementNumber; 31 | 32 | private final Optional sequenceNumber; 33 | 34 | 35 | public StatementNumber(String statementNumber, String sequenceNumber) { 36 | 37 | Preconditions.checkArgument(statementNumber != null, "statementNumber can't be null"); 38 | Preconditions.checkArgument(statementNumber.length() >= 1 && statementNumber.length() <= 5, "expected statementNumber length to be between 1 and 5, but was " + statementNumber.length()); 39 | Preconditions.checkArgument(sequenceNumber == null || sequenceNumber.length() >= 1 && sequenceNumber.length() <= 5, "expected sequenceNumber length to be between 1 and 5, but was " + (sequenceNumber != null ? sequenceNumber.length() : null)); 40 | 41 | this.statementNumber = statementNumber; 42 | this.sequenceNumber = Optional.ofNullable(sequenceNumber); 43 | } 44 | 45 | public static StatementNumber of(GeneralField field) throws FieldNotationParseException { 46 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_28C), "unexpected field tag '%s'", field.getTag()); 47 | 48 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 49 | 50 | String value = subFields.get(0); 51 | String sequenceNumber = subFields.get(1); 52 | 53 | return new StatementNumber(value, sequenceNumber); 54 | } 55 | 56 | public String getStatementNumber() { 57 | return statementNumber; 58 | } 59 | 60 | public Optional getSequenceNumber() { 61 | return sequenceNumber; 62 | } 63 | 64 | @Override 65 | public String getTag() { 66 | return FIELD_TAG_28C; 67 | } 68 | 69 | @Override 70 | public String getContent() { 71 | try { 72 | return SWIFT_NOTATION.render(Lists.newArrayList(statementNumber, sequenceNumber.orElse(null))); 73 | } catch (FieldNotationParseException e) { 74 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/SwiftField.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | /** 4 | * Created by qoomon on 01/07/16. 5 | */ 6 | public interface SwiftField { 7 | 8 | String getTag(); 9 | 10 | String getContent(); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/SwiftFieldReader.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.ImmutableSet; 5 | import com.qoomon.banking.swift.submessage.PageSeparator; 6 | import com.qoomon.banking.swift.submessage.field.exception.FieldLineParseException; 7 | import com.qoomon.banking.swift.submessage.field.exception.FieldParseException; 8 | 9 | import java.io.IOException; 10 | import java.io.LineNumberReader; 11 | import java.io.Reader; 12 | import java.util.Set; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | /** 17 | * Created by qoomon on 27/06/16. 18 | */ 19 | public class SwiftFieldReader { 20 | 21 | private static final Pattern FIELD_STRUCTURE_PATTERN = Pattern.compile(":(?[^:]+):(?.*)"); 22 | 23 | private final static Set FIELD_START_LINE_TYPE_SET = ImmutableSet.of(FieldLineType.FIELD, FieldLineType.SEPARATOR); 24 | 25 | private final LineNumberReader lineReader; 26 | 27 | private FieldLine currentFieldLine = null; 28 | 29 | 30 | public SwiftFieldReader(Reader textReader) { 31 | this.lineReader = new LineNumberReader(textReader); 32 | } 33 | 34 | public int getFieldLineNumber() { 35 | return lineReader.getLineNumber() - 1; 36 | } 37 | 38 | public GeneralField readField() throws FieldParseException { 39 | // field fields 40 | String tag = null; 41 | StringBuilder contentBuilder = new StringBuilder(); 42 | 43 | try { 44 | if (currentFieldLine == null) { 45 | currentFieldLine = readFieldLine(lineReader); 46 | } 47 | if (currentFieldLine == null) { 48 | return null; 49 | } 50 | 51 | Set nextValidFieldLineTypeSet = FIELD_START_LINE_TYPE_SET; 52 | while (currentFieldLine != null) { 53 | ensureValidNextLine(currentFieldLine, nextValidFieldLineTypeSet, lineReader); 54 | 55 | switch (currentFieldLine.getType()) { 56 | case FIELD: { 57 | Matcher fieldMatcher = FIELD_STRUCTURE_PATTERN.matcher(currentFieldLine.getContent()); 58 | if (!fieldMatcher.matches()) { 59 | throw new FieldParseException("Parse error: " + currentFieldLine.getType().name() + " did not match " + FIELD_STRUCTURE_PATTERN.pattern(), getFieldLineNumber()); 60 | } 61 | 62 | // start of a new field 63 | tag = fieldMatcher.group("tag"); 64 | contentBuilder.append(fieldMatcher.group("content")); 65 | nextValidFieldLineTypeSet = ImmutableSet.of(FieldLineType.FIELD, FieldLineType.FIELD_CONTINUATION, FieldLineType.SEPARATOR); 66 | break; 67 | } 68 | case FIELD_CONTINUATION: { 69 | contentBuilder.append("\n"); 70 | contentBuilder.append(currentFieldLine.getContent()); 71 | nextValidFieldLineTypeSet = ImmutableSet.of(FieldLineType.FIELD, FieldLineType.FIELD_CONTINUATION, FieldLineType.SEPARATOR); 72 | break; 73 | } 74 | case SEPARATOR: { 75 | tag = PageSeparator.TAG; 76 | nextValidFieldLineTypeSet = ImmutableSet.of(); 77 | break; 78 | } 79 | default: 80 | throw new FieldParseException("Bug: Missing handling for line type " + currentFieldLine.getType().name(), getFieldLineNumber()); 81 | } 82 | 83 | currentFieldLine = readFieldLine(lineReader); 84 | if (currentFieldLine == null || FIELD_START_LINE_TYPE_SET.contains(currentFieldLine.getType())) { 85 | break; 86 | } 87 | } 88 | 89 | return new GeneralField( 90 | tag, 91 | contentBuilder.toString() 92 | ); 93 | } catch (FieldParseException e) { 94 | throw e; 95 | } catch (Exception e) { 96 | throw new FieldParseException(e.getMessage(), getFieldLineNumber(), e); 97 | } 98 | } 99 | 100 | private void ensureValidNextLine(FieldLine nextFieldLine, Set expectedFieldLineTypeSet, LineNumberReader lineReader) throws FieldParseException { 101 | FieldLineType fieldLineType = nextFieldLine != null ? nextFieldLine.getType() : null; 102 | if (!expectedFieldLineTypeSet.contains(fieldLineType)) { 103 | throw new FieldParseException("Expected FieldLine '" + expectedFieldLineTypeSet + "', but was '" + fieldLineType + "'", lineReader.getLineNumber()); 104 | } 105 | } 106 | 107 | private FieldLineType determineMessageLineType(String messageLine) { 108 | Preconditions.checkArgument(messageLine != null && !messageLine.isEmpty(), "messageLine can't be null or empty"); 109 | 110 | if (messageLine.equals(PageSeparator.TAG)) { 111 | return FieldLineType.SEPARATOR; 112 | } 113 | if (messageLine.startsWith(":")) { 114 | Matcher tagMatcher = FIELD_STRUCTURE_PATTERN.matcher(messageLine); 115 | if (tagMatcher.matches() && tagMatcher.group("tag") != null) { 116 | return FieldLineType.FIELD; 117 | } 118 | } 119 | return FieldLineType.FIELD_CONTINUATION; 120 | 121 | } 122 | 123 | private FieldLine readFieldLine(LineNumberReader lineReader) throws FieldLineParseException { 124 | try { 125 | String line = lineReader.readLine(); 126 | return line == null ? null : new FieldLine(line); 127 | } catch (IOException e) { 128 | throw new FieldLineParseException(e.getMessage(), lineReader.getLineNumber(), e); 129 | } 130 | } 131 | 132 | private class FieldLine { 133 | private FieldLineType type; 134 | private String content; 135 | 136 | public FieldLine(String content) { 137 | this.type = determineMessageLineType(content); 138 | this.content = content; 139 | } 140 | 141 | public FieldLineType getType() { 142 | return type; 143 | } 144 | 145 | public String getContent() { 146 | return content; 147 | } 148 | } 149 | 150 | private enum FieldLineType { 151 | FIELD, 152 | FIELD_CONTINUATION, 153 | SEPARATOR 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/TransactionGroup.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | 5 | import java.util.Optional; 6 | 7 | 8 | public class TransactionGroup { 9 | 10 | /** 11 | * @see StatementLine#FIELD_TAG_61 12 | */ 13 | private final StatementLine statementLine; 14 | 15 | /** 16 | * @see InformationToAccountOwner#FIELD_TAG_86 17 | */ 18 | private final Optional informationToAccountOwner; 19 | 20 | 21 | public TransactionGroup(StatementLine statementLine, InformationToAccountOwner informationToAccountOwner) { 22 | 23 | Preconditions.checkArgument(statementLine != null, "statementLine can't be null"); 24 | 25 | this.statementLine = statementLine; 26 | this.informationToAccountOwner = Optional.ofNullable(informationToAccountOwner); 27 | } 28 | 29 | public StatementLine getStatementLine() { 30 | return statementLine; 31 | } 32 | 33 | public Optional getInformationToAccountOwner() { 34 | return informationToAccountOwner; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/TransactionReferenceNumber.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftNotation; 7 | 8 | import java.util.List; 9 | 10 | 11 | /** 12 | * Transaction Reference Number 13 | *

14 | * Field Tag :20: 15 | *

16 | * Format 20x 17 | *

18 | * SubFields 19 | *

20 |  * 1: 20x    - Value
21 |  * 
22 | */ 23 | public class TransactionReferenceNumber implements SwiftField { 24 | 25 | public static final String FIELD_TAG_20 = "20"; 26 | 27 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("20x"); 28 | 29 | private final String content; 30 | 31 | 32 | public TransactionReferenceNumber(String content) { 33 | 34 | Preconditions.checkArgument(content != null, "content can't be null"); 35 | 36 | this.content = content; 37 | } 38 | 39 | public static TransactionReferenceNumber of(GeneralField field) throws FieldNotationParseException { 40 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_20), "unexpected field tag '%s'", field.getTag()); 41 | 42 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 43 | 44 | String value = subFields.get(0); 45 | 46 | return new TransactionReferenceNumber(value); 47 | } 48 | 49 | 50 | @Override 51 | public String getTag() { 52 | return FIELD_TAG_20; 53 | } 54 | 55 | @Override 56 | public String getContent() { 57 | try { 58 | return SWIFT_NOTATION.render(Lists.newArrayList(content)); 59 | } catch (FieldNotationParseException e) { 60 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/TransactionSummary.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.qoomon.banking.swift.notation.FieldNotationParseException; 6 | import com.qoomon.banking.swift.notation.SwiftDecimalFormatter; 7 | import com.qoomon.banking.swift.notation.SwiftNotation; 8 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 9 | import org.joda.money.BigMoney; 10 | import org.joda.money.CurrencyUnit; 11 | 12 | import java.math.BigDecimal; 13 | import java.util.List; 14 | 15 | /** 16 | * Transaction Summary 17 | *

18 | * Field Tag :90D: - Debit 19 | * Field Tag :90C: - Credit 20 | *

21 | * Format 5n3!a15d 22 | *

23 | * SubFields 24 | *

 25 |  * 1: 5n - Transaction Count
 26 |  * 2: 3!a - Currency - Three Digit Code
 27 |  * 4: 15d - Amount
 28 |  * 
29 | * Example 30 | *
 31 |  * 4USD5782,64
 32 |  * 
33 | */ 34 | public class TransactionSummary implements SwiftField { 35 | 36 | public static final String FIELD_TAG_90D = "90D"; 37 | 38 | public static final String FIELD_TAG_90C = "90C"; 39 | 40 | public static final SwiftNotation SWIFT_NOTATION = new SwiftNotation("5n3!a15d"); 41 | 42 | private final DebitCreditMark debitCreditMark; 43 | 44 | private final int transactionCount; 45 | 46 | private final BigMoney amount; 47 | 48 | 49 | public TransactionSummary(DebitCreditMark debitCreditMark, int transactionCount, BigMoney amount) { 50 | 51 | Preconditions.checkArgument(debitCreditMark != null, "debitCreditMark can't be null"); 52 | Preconditions.checkArgument(transactionCount >= 0, "transaction count can't be negative. was: %s", transactionCount); 53 | Preconditions.checkArgument(amount != null, "amount can't be null"); 54 | Preconditions.checkArgument(amount.isPositiveOrZero(), "amount can't be negative"); 55 | 56 | this.debitCreditMark = debitCreditMark; 57 | this.transactionCount = transactionCount; 58 | this.amount = amount; 59 | } 60 | 61 | public static TransactionSummary of(GeneralField field) throws FieldNotationParseException { 62 | Preconditions.checkArgument(field.getTag().equals(FIELD_TAG_90D) || field.getTag().equals(FIELD_TAG_90C), "unexpected field tag '%s'", field.getTag()); 63 | DebitCreditMark type = field.getTag().equals(FIELD_TAG_90D) ? DebitCreditMark.DEBIT : DebitCreditMark.CREDIT; 64 | 65 | List subFields = SWIFT_NOTATION.parse(field.getContent()); 66 | 67 | int transactionCount = Integer.parseInt(subFields.get(0)); 68 | CurrencyUnit amountCurrency = CurrencyUnit.of(subFields.get(1)); 69 | BigDecimal amountValue = SwiftDecimalFormatter.parse(subFields.get(2)); 70 | BigMoney amount = BigMoney.of(amountCurrency, amountValue); 71 | 72 | return new TransactionSummary(type, transactionCount, amount); 73 | } 74 | 75 | public DebitCreditMark getDebitCreditMark() { 76 | return debitCreditMark; 77 | } 78 | 79 | public int getTransactionCount() { 80 | return transactionCount; 81 | } 82 | 83 | public BigMoney getAmount() { 84 | return amount; 85 | } 86 | 87 | public BigMoney getSignedAmount() { 88 | if(getDebitCreditMark().sign() < 0) { 89 | return amount.negated(); 90 | } 91 | return amount; 92 | } 93 | 94 | @Override 95 | public String getTag() { 96 | return debitCreditMark == DebitCreditMark.DEBIT ? FIELD_TAG_90D : FIELD_TAG_90C; 97 | } 98 | 99 | @Override 100 | public String getContent() { 101 | try { 102 | return SWIFT_NOTATION.render(Lists.newArrayList( 103 | String.valueOf(transactionCount), 104 | amount.getCurrencyUnit().getCode(), 105 | SwiftDecimalFormatter.format(amount.getAmount()) 106 | )); 107 | } catch (FieldNotationParseException e) { 108 | throw new IllegalStateException("Invalid field values within " + getClass().getSimpleName() + " instance", e); 109 | } 110 | } 111 | 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/exception/FieldLineParseException.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field.exception; 2 | 3 | /** 4 | * Created by qoomon on 27/06/16. 5 | */ 6 | public class FieldLineParseException extends Exception { 7 | 8 | private final int lineNumber; 9 | 10 | public FieldLineParseException(String message, int lineNumber, Throwable cause) { 11 | super(message + " at line number " + lineNumber, cause); 12 | this.lineNumber = lineNumber; 13 | } 14 | 15 | public int getLineNumber() { 16 | return lineNumber; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/exception/FieldParseException.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field.exception; 2 | 3 | /** 4 | * Created by qoomon on 27/06/16. 5 | */ 6 | public class FieldParseException extends Exception { 7 | 8 | private final int lineNumber; 9 | 10 | public FieldParseException(String message, int lineNumber) { 11 | super(message + " at line number " + lineNumber); 12 | this.lineNumber = lineNumber; 13 | } 14 | 15 | public FieldParseException(String message, int lineNumber, Throwable cause) { 16 | super(message + " at line number " + lineNumber, cause); 17 | this.lineNumber = lineNumber; 18 | } 19 | 20 | public FieldParseException(Throwable cause) { 21 | super(cause); 22 | lineNumber = 0; 23 | } 24 | 25 | public int getLineNumber() { 26 | return lineNumber; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/subfield/DebitCreditMark.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field.subfield; 2 | 3 | /** 4 | * Created by qoomon on 05/07/16. 5 | */ 6 | public enum DebitCreditMark { 7 | 8 | DEBIT, 9 | CREDIT; 10 | 11 | public static DebitCreditMark ofFieldValue(String value) { 12 | switch (value) { 13 | case "D": 14 | return DEBIT; 15 | case "C": 16 | return CREDIT; 17 | default: 18 | throw new IllegalArgumentException("No mapping found for value '" + value + "'"); 19 | } 20 | } 21 | 22 | public String toFieldValue() { 23 | switch (this) { 24 | case DEBIT: 25 | return "D"; 26 | case CREDIT: 27 | return "C"; 28 | default: 29 | throw new IllegalStateException("No field value mapping for " + this.name()); 30 | } 31 | } 32 | 33 | /** 34 | * Return sign factor for mark. 35 | * @return -1 for negative sign or +1 for positive sign 36 | */ 37 | public int sign() { 38 | if (this == DebitCreditMark.DEBIT) { 39 | return -1; 40 | } 41 | 42 | if (this == DebitCreditMark.CREDIT) { 43 | return 1; 44 | } 45 | 46 | throw new IllegalAccessError("Unmapped sign for mark: " + this.name()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/subfield/DebitCreditType.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field.subfield; 2 | 3 | /** 4 | * Created by qoomon on 18/08/16. 5 | */ 6 | public enum DebitCreditType { 7 | REGULAR, 8 | REVERSAL 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/subfield/MessagePriority.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field.subfield; 2 | 3 | /** 4 | * U = Urgent, N = Normal, S = System 5 | */ 6 | public enum MessagePriority { 7 | 8 | URGENT, 9 | NORMAL, 10 | SYSTEM; 11 | 12 | public static MessagePriority of(String value) { 13 | switch (value) { 14 | case "URGENT": 15 | case "U": 16 | return URGENT; 17 | case "NORMAL": 18 | case "N": 19 | case "": 20 | return NORMAL; 21 | case "SYSTEM": 22 | case "S": 23 | return SYSTEM; 24 | default: 25 | throw new IllegalArgumentException("No mapping found for value '" + value + "'"); 26 | } 27 | } 28 | 29 | public String asText(){ 30 | switch (this) { 31 | case URGENT: 32 | return "U"; 33 | case NORMAL: 34 | return "N"; 35 | case SYSTEM: 36 | return "S"; 37 | default: 38 | throw new IllegalArgumentException("No mapping found for value '" + this + "'"); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/qoomon/banking/swift/submessage/field/subfield/TransactionTypeIdentificationCode.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field.subfield; 2 | 3 | import com.google.common.base.Preconditions; 4 | 5 | /** 6 | * BNK, // Securities Related Item – Bank fees 7 | * BOE, // Bill of exchange 8 | * BRF, // Brokerage fee 9 | * CAR, // Securities Related Item – Corporate Actions Related (Should only be used when no specific corporate action event code is available) 10 | * CAS, // Securities Related Item – Cash in Lieu 11 | * CHG, // Charges and other expenses 12 | * CHK, // Cheques 13 | * CLR, // Cash letters/Cheques remittance 14 | * CMI, // Cash management item – No detail 15 | * CMN, // Cash management item – Notional pooling 16 | * CMP, // Compensation claims 17 | * CMS, // Cash management item – Sweeping 18 | * CMT, // Cash management item -Topping 19 | * CMZ, // Cash management item – Zero balancing 20 | * COL, // Collections (used when entering a principal amount) 21 | * COM, // Commission 22 | * CPN, // Securities Related Item – Coupon payments 23 | * DCR, // Documentary credit (used when entering a principal amount) 24 | * DDT, // Direct Debit Item 25 | * DIS, // Securities Related Item – Gains disbursement 26 | * DIV, // Securities Related Item – Dividends 27 | * EQA, // Equivalent amount 28 | * EXT, // Securities Related Item – External transfer for own account 29 | * FEX, // Foreign exchange 30 | * INT, // Interest 31 | * LBX, // Lock box 32 | * LDP, // Loan deposit 33 | * MAR, // Securities Related Item – Margin payments/Receipts 34 | * MAT, // Securities Related Item – Maturity 35 | * MGT, // Securities Related Item – Management fees 36 | * MSC, // Miscellaneous 37 | * NWI, // Securities Related Item – New issues distribution 38 | * ODC, // Overdraft charge 39 | * OPT, // Securities Related Item – Options 40 | * PCH, // Securities Related Item – Purchase (including STIF and Time deposits) 41 | * POP, // Securities Related Item – Pair-off proceeds 42 | * PRN, // Securities Related Item – Principal pay-down/pay-up 43 | * REC, // Securities Related Item – Tax reclaim 44 | * RED, // Securities Related Item – Redemption/Withdrawal 45 | * RIG, // Securities Related Item – Rights 46 | * RTI, // Returned item 47 | * SAL, // Securities Related Item – Sale (including STIF and Time deposits) 48 | * SEC, // Securities (used when entering a principal amount) 49 | * SLE, // Securities Related Item – Securities lending related 50 | * STO, // Standing order 51 | * STP, // Securities Related Item – Stamp duty 52 | * SUB, // Securities Related Item – Subscription 53 | * SWP, // Securities Related Item – SWAP payment 54 | * TAX, // Securities Related Item – Withholding tax payment 55 | * TCK, // Travellers cheques 56 | * TCM, // Securities Related Item – Tripartite collateral management 57 | * TRA, // Securities Related Item – Internal transfer for own account 58 | * TRF, // Transfer 59 | * TRN, // Securities Related Item – Transaction fee 60 | * UWC, // Securities Related Item – Underwriting commission 61 | * VDA, // Value date adjustment (used with an entry made to withdraw an incorrectly dated entry – it will be followed by the correct entry with the relevant code) 62 | * WAR // Securities Related Item – Warrant 63 | */ 64 | public class TransactionTypeIdentificationCode { 65 | 66 | private final IdentificationType type; 67 | private final String code; 68 | 69 | TransactionTypeIdentificationCode(IdentificationType type, String code) { 70 | 71 | Preconditions.checkArgument(type != null, "type can't be null"); 72 | Preconditions.checkArgument(code != null, "code can't be null"); 73 | 74 | this.type = type; 75 | this.code = code; 76 | } 77 | 78 | public static TransactionTypeIdentificationCode of(String text) { 79 | IdentificationType type = IdentificationType.valueOf(text.substring(0, 1)); 80 | String code = text.substring(1); 81 | return new TransactionTypeIdentificationCode(type, code); 82 | } 83 | 84 | 85 | public enum IdentificationType { 86 | F, 87 | N, 88 | S 89 | } 90 | 91 | public IdentificationType getType() { 92 | return type; 93 | } 94 | 95 | public String getCode() { 96 | return code; 97 | } 98 | 99 | 100 | @Override 101 | public boolean equals(Object o) { 102 | if (this == o) { 103 | return true; 104 | } 105 | if (o == null || getClass() != o.getClass()) { 106 | return false; 107 | } 108 | 109 | TransactionTypeIdentificationCode that = (TransactionTypeIdentificationCode) o; 110 | 111 | if (type != that.type) { 112 | return false; 113 | } 114 | return code.equals(that.code); 115 | 116 | } 117 | 118 | @Override 119 | public int hashCode() { 120 | int result = type.hashCode(); 121 | result = 31 * result + code.hashCode(); 122 | return result; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/TestUtils.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | import java.util.concurrent.Callable; 6 | 7 | /** 8 | * Created by qoomon on 13/07/16. 9 | */ 10 | public class TestUtils { 11 | 12 | public static List collectUntilNull(Callable function) throws Exception { 13 | List resultList = new LinkedList<>(); 14 | T result; 15 | while ((result = function.call()) != null) { 16 | resultList.add(result); 17 | } 18 | return resultList; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/bic/BICTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.bic; 2 | 3 | import com.qoomon.banking.bic.BIC; 4 | import org.junit.Test; 5 | 6 | import static org.assertj.core.api.Assertions.*; 7 | 8 | /** 9 | * Created by qoomon on 19/07/16. 10 | */ 11 | public class BICTest { 12 | 13 | @Test 14 | public void of_WHEN_valid_bic_RETURN_bic() throws Exception { 15 | 16 | // Given 17 | String bicText = "HASPDEHHXXX"; 18 | 19 | // When 20 | BIC bic = BIC.of(bicText); 21 | 22 | // Then 23 | assertThat(bic).isNotNull(); 24 | assertThat(bic.getInstitutionCode()).isEqualTo("HASP"); 25 | assertThat(bic.getCountryCode()).isEqualTo("DE"); 26 | assertThat(bic.getLocationCode()).isEqualTo("HH"); 27 | assertThat(bic.getBranchCode()).contains("XXX"); 28 | 29 | } 30 | 31 | @Test 32 | public void of_WHEN_valid_bic_without_branche_code_RETURN_bic() throws Exception { 33 | 34 | // Given 35 | String bicText = "HASPDEHH"; 36 | 37 | // When 38 | BIC bic = BIC.of(bicText); 39 | 40 | // Then 41 | assertThat(bic).isNotNull(); 42 | assertThat(bic.getInstitutionCode()).isEqualTo("HASP"); 43 | assertThat(bic.getCountryCode()).isEqualTo("DE"); 44 | assertThat(bic.getLocationCode()).isEqualTo("HH"); 45 | assertThat(bic.getBranchCode()).isNotPresent(); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/message/block/ApplicationHeaderBlockTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.qoomon.banking.swift.submessage.field.subfield.MessagePriority; 4 | import org.assertj.core.api.SoftAssertions; 5 | import org.junit.Test; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | import static org.assertj.core.api.Assertions.*; 10 | import static org.assertj.core.api.ThrowableAssert.catchThrowable; 11 | 12 | 13 | public class ApplicationHeaderBlockTest { 14 | 15 | @Test 16 | public void of_WHEN_valid_output_block_is_passed_RETURN_new_block() throws Exception { 17 | 18 | // Given 19 | GeneralBlock generalBlock = new GeneralBlock(ApplicationHeaderBlock.BLOCK_ID_2, "O9401506110804LRLRXXXX4A1100009040831108041707N"); 20 | 21 | // When 22 | ApplicationHeaderBlock block = ApplicationHeaderBlock.of(generalBlock); 23 | 24 | // Then 25 | assertThat(block).isNotNull(); 26 | assertThat(block.getOutput()).isPresent(); 27 | if (block.getOutput().isPresent()) { 28 | SoftAssertions softly = new SoftAssertions(); 29 | ApplicationHeaderOutputBlock outputBlock = block.getOutput().get(); 30 | softly.assertThat(outputBlock.getMessageType()).isEqualTo("940"); 31 | softly.assertThat(outputBlock.getInputDateTime()).isEqualTo(LocalDateTime.of(2011, 8, 4, 15, 6)); 32 | softly.assertThat(outputBlock.getInputReference()).isEqualTo("LRLRXXXX4A11"); 33 | softly.assertThat(outputBlock.getSessionNumber()).isEqualTo("0000"); 34 | softly.assertThat(outputBlock.getSequenceNumber()).isEqualTo("904083"); 35 | softly.assertThat(outputBlock.getOutputDateTime()).isEqualTo(LocalDateTime.of(2011, 8, 4, 17, 7)); 36 | softly.assertThat(outputBlock.getMessagePriority()).isEqualTo(MessagePriority.NORMAL); 37 | softly.assertAll(); 38 | } 39 | 40 | assertThat(block.getInput()).isNotPresent(); 41 | } 42 | 43 | @Test 44 | public void of_WHEN_valid_output_block_is_passed_without_optional_priority_RETURN_new_block() throws Exception { 45 | 46 | // Given 47 | GeneralBlock generalBlock = new GeneralBlock(ApplicationHeaderBlock.BLOCK_ID_2, "O9401506110804LRLRXXXX4A1100009040831108041707"); 48 | 49 | // When 50 | ApplicationHeaderBlock block = ApplicationHeaderBlock.of(generalBlock); 51 | 52 | // Then 53 | assertThat(block).isNotNull(); 54 | assertThat(block.getOutput()).isPresent(); 55 | if (block.getOutput().isPresent()) { 56 | ApplicationHeaderOutputBlock outputBlock = block.getOutput().get(); 57 | assertThat(outputBlock.getMessagePriority()).isEqualTo(MessagePriority.NORMAL); 58 | } 59 | 60 | assertThat(block.getInput()).isNotPresent(); 61 | } 62 | 63 | @Test 64 | public void of_WHEN_valid_input_block_is_passed_RETURN_new_block() throws Exception { 65 | 66 | // Given 67 | GeneralBlock generalBlock = new GeneralBlock(ApplicationHeaderBlock.BLOCK_ID_2, "I101YOURBANKXJKLU3003"); 68 | 69 | // When 70 | ApplicationHeaderBlock block = ApplicationHeaderBlock.of(generalBlock); 71 | 72 | // Then 73 | assertThat(block).isNotNull(); 74 | assertThat(block.getInput()).isPresent(); 75 | if (block.getInput().isPresent()) { 76 | SoftAssertions softly = new SoftAssertions(); 77 | ApplicationHeaderInputBlock inputBlock = block.getInput().get(); 78 | softly.assertThat(inputBlock.getMessageType()).isEqualTo("101"); 79 | softly.assertThat(inputBlock.getReceiverAddress()).isEqualTo("YOURBANKXJKL"); 80 | softly.assertThat(inputBlock.getMessagePriority()).isEqualTo(MessagePriority.URGENT); 81 | softly.assertThat(inputBlock.getDeliveryMonitoring()).contains("3"); 82 | softly.assertThat(inputBlock.getObsolescencePeriod()).contains("003"); 83 | softly.assertAll(); 84 | } 85 | 86 | assertThat(block.getOutput()).isNotPresent(); 87 | } 88 | 89 | @Test 90 | public void getContent_output_block_SHOULD_return_input_text() throws Exception { 91 | 92 | // Given 93 | String contentInput = "O9401506110804LRLRXXXX4A1100009040831108041707N"; 94 | GeneralBlock generalBlock = new GeneralBlock(ApplicationHeaderBlock.BLOCK_ID_2, contentInput); 95 | ApplicationHeaderBlock classUnderTest = ApplicationHeaderBlock.of(generalBlock); 96 | 97 | // When 98 | String content = classUnderTest.getContent(); 99 | 100 | // Then 101 | assertThat(content).isEqualTo(contentInput); 102 | } 103 | 104 | @Test 105 | public void getContent_input_block_SHOULD_return_input_text() throws Exception { 106 | 107 | // Given 108 | String contentInput = "I101YOURBANKXJKLU3003"; 109 | GeneralBlock generalBlock = new GeneralBlock(ApplicationHeaderBlock.BLOCK_ID_2, contentInput); 110 | ApplicationHeaderBlock classUnderTest = ApplicationHeaderBlock.of(generalBlock); 111 | 112 | // When 113 | String content = classUnderTest.getContent(); 114 | 115 | // Then 116 | assertThat(content).isEqualTo(contentInput); 117 | } 118 | 119 | @Test 120 | public void of_WHEN_block_with_invalid_id_is_passed_THROW_exception() throws Exception { 121 | 122 | // Given 123 | GeneralBlock generalBlock = new GeneralBlock("0", "\nabc\n-"); 124 | 125 | // When 126 | Throwable exception = catchThrowable(() -> ApplicationHeaderBlock.of(generalBlock)); 127 | 128 | // Then 129 | assertThat(exception).isInstanceOf(IllegalArgumentException.class); 130 | } 131 | 132 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/message/block/BasicHeaderBlockTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import org.assertj.core.api.SoftAssertions; 4 | import org.junit.Test; 5 | 6 | import static org.assertj.core.api.Assertions.*; 7 | import static org.assertj.core.api.ThrowableAssert.catchThrowable; 8 | 9 | /** 10 | * Created by qoomon on 14/07/16. 11 | */ 12 | public class BasicHeaderBlockTest { 13 | 14 | @Test 15 | public void of_WHEN_valid_block_is_passed_RETURN_new_block() throws Exception { 16 | 17 | // Given 18 | GeneralBlock generalBlock = new GeneralBlock(BasicHeaderBlock.BLOCK_ID_1, "F01YOURCODEZABC2222777777"); 19 | 20 | // When 21 | BasicHeaderBlock block = BasicHeaderBlock.of(generalBlock); 22 | 23 | // Then 24 | assertThat(block).isNotNull(); 25 | SoftAssertions softly = new SoftAssertions(); 26 | softly.assertThat(block.getApplicationId()).isEqualTo("F"); 27 | softly.assertThat(block.getServiceId()).isEqualTo("01"); 28 | softly.assertThat(block.getLogicalTerminalAddress()).isEqualTo("YOURCODEZABC"); 29 | softly.assertThat(block.getSessionNumber()).isEqualTo("2222"); 30 | softly.assertThat(block.getSequenceNumber()).isEqualTo("777777"); 31 | softly.assertAll(); 32 | } 33 | 34 | @Test 35 | public void of_WHEN_block_with_invalid_id_is_passed_THROW_exception() throws Exception { 36 | 37 | // Given 38 | GeneralBlock generalBlock = new GeneralBlock("0", "\nabc\n-"); 39 | 40 | // When 41 | Throwable exception = catchThrowable(() -> BasicHeaderBlock.of(generalBlock)); 42 | 43 | // Then 44 | assertThat(exception).isInstanceOf(IllegalArgumentException.class); 45 | } 46 | 47 | @Test 48 | public void getContent_SHOULD_return_input_text() throws Exception { 49 | 50 | // Given 51 | String contentInput = "F01YOURCODEZABC2222777777"; 52 | GeneralBlock generalBlock = new GeneralBlock(BasicHeaderBlock.BLOCK_ID_1, contentInput); 53 | BasicHeaderBlock classUnderTest = BasicHeaderBlock.of(generalBlock); 54 | // When 55 | String content = classUnderTest.getContent(); 56 | 57 | // Then 58 | assertThat(content).isEqualTo(contentInput); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/message/block/SwiftBlockReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.qoomon.banking.TestUtils; 4 | import com.qoomon.banking.swift.message.block.exception.BlockParseException; 5 | import org.assertj.core.api.Assertions; 6 | import org.junit.Test; 7 | 8 | import java.io.StringReader; 9 | import java.util.List; 10 | 11 | import static org.assertj.core.api.Assertions.*; 12 | 13 | /** 14 | * Created by qoomon on 26/07/16. 15 | */ 16 | public class SwiftBlockReaderTest { 17 | 18 | @Test 19 | public void readBlock_SHOULD_read_valid_blocks() throws Exception { 20 | // Given 21 | 22 | String blockText = "{1:a}{2:b}{3:c}"; 23 | 24 | SwiftBlockReader subjectUnderTest = new SwiftBlockReader(new StringReader(blockText)); 25 | 26 | // When 27 | List blockList = TestUtils.collectUntilNull(subjectUnderTest::readBlock); 28 | 29 | // Then 30 | 31 | assertThat(blockList).hasSize(3); 32 | assertThat(blockList.get(0).getId()).isEqualTo("1"); 33 | assertThat(blockList.get(0).getContent()).isEqualTo("a"); 34 | assertThat(blockList.get(1).getId()).isEqualTo("2"); 35 | assertThat(blockList.get(1).getContent()).isEqualTo("b"); 36 | assertThat(blockList.get(2).getId()).isEqualTo("3"); 37 | assertThat(blockList.get(2).getContent()).isEqualTo("c"); 38 | } 39 | 40 | @Test 41 | public void readBlock_WHEN_unfinished_block_detected_THROW_exception() throws Exception { 42 | // Given 43 | 44 | String blockText = "{1:a}{2:b}{3:c"; 45 | 46 | SwiftBlockReader subjectUnderTest = new SwiftBlockReader(new StringReader(blockText)); 47 | 48 | // When 49 | Throwable exception = catchThrowable(() -> TestUtils.collectUntilNull(subjectUnderTest::readBlock)); 50 | 51 | // Then 52 | assertThat(exception).isInstanceOf(BlockParseException.class); 53 | } 54 | 55 | @Test 56 | public void readBlock_SHOULD_handle_CRLF_line_endings() throws Exception { 57 | // Given 58 | 59 | String blockText = "{1:a}{2:b}{3:c}{4:\r\n-}"; 60 | 61 | SwiftBlockReader subjectUnderTest = new SwiftBlockReader(new StringReader(blockText)); 62 | 63 | // When 64 | List blockList = TestUtils.collectUntilNull(subjectUnderTest::readBlock); 65 | 66 | // Then 67 | 68 | assertThat(blockList).hasSize(4); 69 | assertThat(blockList.get(0).getId()).isEqualTo("1"); 70 | assertThat(blockList.get(0).getContent()).isEqualTo("a"); 71 | assertThat(blockList.get(1).getId()).isEqualTo("2"); 72 | assertThat(blockList.get(1).getContent()).isEqualTo("b"); 73 | assertThat(blockList.get(2).getId()).isEqualTo("3"); 74 | assertThat(blockList.get(2).getContent()).isEqualTo("c"); 75 | assertThat(blockList.get(3).getId()).isEqualTo("4"); 76 | assertThat(blockList.get(3).getContent()).isEqualTo("\n-"); 77 | } 78 | 79 | @Test 80 | public void readBlock_SHOULD_handle_LF_line_endings() throws Exception { 81 | // Given 82 | 83 | String blockText = "{1:a}{2:b}{3:c}{4:\n-}"; 84 | 85 | SwiftBlockReader subjectUnderTest = new SwiftBlockReader(new StringReader(blockText)); 86 | 87 | // When 88 | List blockList = TestUtils.collectUntilNull(subjectUnderTest::readBlock); 89 | 90 | // Then 91 | 92 | assertThat(blockList).hasSize(4); 93 | assertThat(blockList.get(0).getId()).isEqualTo("1"); 94 | assertThat(blockList.get(0).getContent()).isEqualTo("a"); 95 | assertThat(blockList.get(1).getId()).isEqualTo("2"); 96 | assertThat(blockList.get(1).getContent()).isEqualTo("b"); 97 | assertThat(blockList.get(2).getId()).isEqualTo("3"); 98 | assertThat(blockList.get(2).getContent()).isEqualTo("c"); 99 | assertThat(blockList.get(3).getId()).isEqualTo("4"); 100 | assertThat(blockList.get(3).getContent()).isEqualTo("\n-"); 101 | } 102 | 103 | @Test 104 | public void readBlock_SHOULD_ignore_whitespaces_between_blocks() throws Exception { 105 | // Given 106 | 107 | String blockText = "{1:a} \t\n\r{2:b}{3:c}"; 108 | 109 | SwiftBlockReader subjectUnderTest = new SwiftBlockReader(new StringReader(blockText)); 110 | 111 | // When 112 | List blockList = TestUtils.collectUntilNull(subjectUnderTest::readBlock); 113 | 114 | // Then 115 | 116 | assertThat(blockList).hasSize(3); 117 | assertThat(blockList.get(0).getId()).isEqualTo("1"); 118 | assertThat(blockList.get(0).getContent()).isEqualTo("a"); 119 | assertThat(blockList.get(1).getId()).isEqualTo("2"); 120 | assertThat(blockList.get(1).getContent()).isEqualTo("b"); 121 | assertThat(blockList.get(2).getId()).isEqualTo("3"); 122 | assertThat(blockList.get(2).getContent()).isEqualTo("c"); 123 | } 124 | 125 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/message/block/SystemTrailerBlockTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import org.assertj.core.api.SoftAssertions; 4 | import org.junit.Test; 5 | 6 | import static org.assertj.core.api.Assertions.*; 7 | import static org.assertj.core.api.ThrowableAssert.catchThrowable; 8 | 9 | /** 10 | * Created by qoomon on 14/07/16. 11 | */ 12 | public class SystemTrailerBlockTest { 13 | 14 | @Test 15 | public void of_WHEN_valid_block_is_passed_RETURN_new_block() throws Exception { 16 | 17 | // Given 18 | GeneralBlock generalBlock = new GeneralBlock(SystemTrailerBlock.BLOCK_ID_S, "{CHK:F7C4F89AF66D}{TNG:}{SAC:}{COP:P}"); 19 | 20 | // When 21 | SystemTrailerBlock block = SystemTrailerBlock.of(generalBlock); 22 | 23 | // Then 24 | assertThat(block).isNotNull(); 25 | SoftAssertions softly = new SoftAssertions(); 26 | softly.assertThat(block.getChecksum()).contains("F7C4F89AF66D"); 27 | softly.assertThat(block.getTraining()).contains(""); 28 | softly.assertThat(block.getAdditionalSubblocks("COP").getContent()).isEqualTo("P"); 29 | softly.assertAll(); 30 | } 31 | 32 | @Test 33 | public void of_WHEN_block_with_invalid_id_is_passed_THROW_exception() throws Exception { 34 | 35 | // Given 36 | GeneralBlock generalBlock = new GeneralBlock("0", "\nabc\n-"); 37 | 38 | // When 39 | Throwable exception = catchThrowable(() -> SystemTrailerBlock.of(generalBlock)); 40 | 41 | // Then 42 | assertThat(exception).isInstanceOf(IllegalArgumentException.class); 43 | } 44 | 45 | @Test 46 | public void getContent_SHOULD_return_input_text() throws Exception { 47 | 48 | // Given 49 | String contentInput = "{CHK:F7C4F89AF66D}{TNG:}{SAC:}{COP:P}"; 50 | GeneralBlock generalBlock = new GeneralBlock(SystemTrailerBlock.BLOCK_ID_S, contentInput); 51 | SystemTrailerBlock classUnderTest = SystemTrailerBlock.of(generalBlock); 52 | 53 | // When 54 | String content = classUnderTest.getContent(); 55 | 56 | // Then 57 | assertThat(content).isEqualTo(contentInput); 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/message/block/TextBlockTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import com.qoomon.banking.swift.message.block.exception.BlockFieldParseException; 4 | import org.junit.Test; 5 | 6 | import static org.assertj.core.api.Assertions.*; 7 | import static org.assertj.core.api.ThrowableAssert.catchThrowable; 8 | 9 | /** 10 | * Created by qoomon on 07/07/16. 11 | */ 12 | public class TextBlockTest { 13 | 14 | @Test 15 | public void of_WHEN_valid_block_with_info_line_is_passed_RETURN_new_block() throws Exception { 16 | 17 | // Given 18 | GeneralBlock generalBlock = new GeneralBlock(TextBlock.BLOCK_ID_4, "info\nabc\n-"); 19 | 20 | // When 21 | TextBlock block = TextBlock.of(generalBlock); 22 | 23 | // Then 24 | assertThat(block).isNotNull(); 25 | assertThat(block.getInfoLine()).hasValue("info"); 26 | assertThat(block.getText()).isEqualTo("abc\n-"); 27 | } 28 | 29 | @Test 30 | public void of_WHEN_valid_block_is_passed_RETURN_new_block() throws Exception { 31 | 32 | // Given 33 | GeneralBlock generalBlock = new GeneralBlock(TextBlock.BLOCK_ID_4, "\nabc\n-"); 34 | 35 | // When 36 | TextBlock block = TextBlock.of(generalBlock); 37 | 38 | // Then 39 | assertThat(block).isNotNull(); 40 | assertThat(block.getInfoLine()).isNotPresent(); 41 | assertThat(block.getText()).isEqualTo("abc\n-"); 42 | } 43 | 44 | @Test 45 | public void of_WHEN_block_with_invalid_id_is_passed_THROW_exception() throws Exception { 46 | 47 | // Given 48 | GeneralBlock generalBlock = new GeneralBlock("0", "\nabc\n-"); 49 | 50 | // When 51 | Throwable exception = catchThrowable(() -> TextBlock.of(generalBlock)); 52 | 53 | // Then 54 | assertThat(exception).isInstanceOf(IllegalArgumentException.class); 55 | } 56 | 57 | @Test 58 | public void of_WHEN_block_with_invalid_ending_is_passed_THROW_exception() throws Exception { 59 | 60 | // Given 61 | GeneralBlock generalBlock = new GeneralBlock(TextBlock.BLOCK_ID_4, "\nabc"); 62 | 63 | // When 64 | Throwable exception = catchThrowable(() -> TextBlock.of(generalBlock)); 65 | 66 | // Then 67 | assertThat(exception).isInstanceOf(BlockFieldParseException.class); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/message/block/UserHeaderBlockTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import org.assertj.core.api.SoftAssertions; 4 | import org.junit.Test; 5 | 6 | import static org.assertj.core.api.Assertions.*; 7 | import static org.assertj.core.api.ThrowableAssert.catchThrowable; 8 | 9 | /** 10 | * Created by qoomon on 14/07/16. 11 | */ 12 | public class UserHeaderBlockTest { 13 | 14 | @Test 15 | public void of_WHEN_valid_block_is_passed_RETURN_new_block() throws Exception { 16 | 17 | // Given 18 | GeneralBlock generalBlock = new GeneralBlock(UserHeaderBlock.BLOCK_ID_3, "{113:SEPA}{108:ILOVESEPA}"); 19 | 20 | // When 21 | UserHeaderBlock block = UserHeaderBlock.of(generalBlock); 22 | 23 | // Then 24 | assertThat(block).isNotNull(); 25 | SoftAssertions softly = new SoftAssertions(); 26 | softly.assertThat(block.getBankingPriorityCode()).contains("SEPA"); 27 | softly.assertThat(block.getMessageUserReference()).contains("ILOVESEPA"); 28 | softly.assertAll(); 29 | } 30 | 31 | @Test 32 | public void of_WHEN_block_with_invalid_id_is_passed_THROW_exception() throws Exception { 33 | 34 | // Given 35 | GeneralBlock generalBlock = new GeneralBlock("0", "\nabc\n-"); 36 | 37 | // When 38 | Throwable exception = catchThrowable(() -> UserHeaderBlock.of(generalBlock)); 39 | 40 | // Then 41 | assertThat(exception).isInstanceOf(IllegalArgumentException.class); 42 | } 43 | 44 | @Test 45 | public void getContent_SHOULD_return_input_text() throws Exception { 46 | 47 | // Given 48 | String contentInput = "{113:SEPA}{108:ILOVESEPA}"; 49 | GeneralBlock generalBlock = new GeneralBlock(UserHeaderBlock.BLOCK_ID_3, contentInput); 50 | UserHeaderBlock classUnderTest = UserHeaderBlock.of(generalBlock); 51 | 52 | // When 53 | String content = classUnderTest.getContent(); 54 | 55 | // Then 56 | assertThat(content).isEqualTo(contentInput); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/message/block/UserTrailerBlockTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.message.block; 2 | 3 | import org.assertj.core.api.SoftAssertions; 4 | import org.junit.Test; 5 | 6 | import static org.assertj.core.api.Assertions.*; 7 | import static org.assertj.core.api.ThrowableAssert.catchThrowable; 8 | 9 | /** 10 | * Created by qoomon on 14/07/16. 11 | */ 12 | public class UserTrailerBlockTest { 13 | 14 | @Test 15 | public void of_WHEN_valid_block_is_passed_RETURN_new_block() throws Exception { 16 | 17 | // Given 18 | GeneralBlock generalBlock = new GeneralBlock(UserTrailerBlock.BLOCK_ID_5, "{CHK:F7C4F89AF66D}{TNG:}"); 19 | 20 | // When 21 | UserTrailerBlock block = UserTrailerBlock.of(generalBlock); 22 | 23 | // Then 24 | assertThat(block).isNotNull(); 25 | SoftAssertions softly = new SoftAssertions(); 26 | softly.assertThat(block.getChecksum()).contains("F7C4F89AF66D"); 27 | softly.assertThat(block.getTraining()).contains(""); 28 | softly.assertAll(); 29 | } 30 | 31 | @Test 32 | public void of_WHEN_block_with_invalid_id_is_passed_THROW_exception() throws Exception { 33 | 34 | // Given 35 | GeneralBlock generalBlock = new GeneralBlock("0", "\nabc\n-"); 36 | 37 | // When 38 | Throwable exception = catchThrowable(() -> UserTrailerBlock.of(generalBlock)); 39 | 40 | // Then 41 | assertThat(exception).isInstanceOf(IllegalArgumentException.class); 42 | } 43 | 44 | @Test 45 | public void getContent_SHOULD_return_input_text() throws Exception { 46 | 47 | // Given 48 | String contentInput = "{CHK:F7C4F89AF66D}{TNG:}"; 49 | GeneralBlock generalBlock = new GeneralBlock(UserTrailerBlock.BLOCK_ID_5, contentInput); 50 | UserTrailerBlock classUnderTest = UserTrailerBlock.of(generalBlock); 51 | 52 | // When 53 | String content = classUnderTest.getContent(); 54 | 55 | // Then 56 | assertThat(content).isEqualTo(contentInput); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/notation/SwiftNotationTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.notation; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.List; 6 | 7 | import static org.assertj.core.api.Assertions.*; 8 | 9 | /** 10 | * Created by qoomon on 29/06/16. 11 | */ 12 | public class SwiftNotationTest { 13 | 14 | @Test 15 | public void parse() throws Exception { 16 | // Given 17 | 18 | String swiftFieldNotation = "1!a6!n3!a15d"; // Tag 60a – Opening Balance 19 | 20 | String fieldText = "A" + "123456" + "ABC" + "1234,"; 21 | 22 | // When 23 | 24 | List fieldValueList = new SwiftNotation(swiftFieldNotation).parse(fieldText); 25 | 26 | // Then 27 | 28 | assertThat(fieldValueList).hasSize(4); 29 | assertThat(fieldValueList.get(0)).isEqualTo("A"); 30 | assertThat(fieldValueList.get(1)).isEqualTo("123456"); 31 | assertThat(fieldValueList.get(2)).isEqualTo("ABC"); 32 | assertThat(fieldValueList.get(3)).isEqualTo("1234,"); 33 | } 34 | 35 | @Test 36 | public void parse_SHOULD_accept_multiline_subfields() throws Exception { 37 | 38 | // Given 39 | 40 | String swiftFieldNotation = "3*5a"; // Tag 60a – Opening Balance 41 | 42 | String fieldText = "A\nAA\nAAA"; 43 | 44 | // When 45 | 46 | List fieldValueList = new SwiftNotation(swiftFieldNotation).parse(fieldText); 47 | 48 | // Then 49 | 50 | assertThat(fieldValueList).hasSize(1); 51 | assertThat(fieldValueList.get(0)).isEqualTo(fieldText); 52 | 53 | } 54 | 55 | @Test 56 | public void parse_SHOULD_ignore_field_separators() throws Exception { 57 | 58 | // Given 59 | 60 | String swiftFieldNotation = "5n[/5n]"; 61 | 62 | String fieldText = "123/11"; 63 | 64 | // When 65 | 66 | List fieldValueList = new SwiftNotation(swiftFieldNotation).parse(fieldText); 67 | 68 | // Then 69 | 70 | assertThat(fieldValueList).hasSize(2); 71 | assertThat(fieldValueList.get(0)).isEqualTo("123"); 72 | assertThat(fieldValueList.get(1)).isEqualTo("11"); 73 | 74 | } 75 | 76 | @Test 77 | public void parse_SHOULD_parse_range_notation() throws Exception { 78 | 79 | // Given 80 | 81 | String swiftFieldNotation = "2-4a"; 82 | 83 | String fieldText = "ABC"; 84 | 85 | // When 86 | 87 | List fieldValueList = new SwiftNotation(swiftFieldNotation).parse(fieldText); 88 | 89 | // Then 90 | 91 | assertThat(fieldValueList).hasSize(1); 92 | assertThat(fieldValueList.get(0)).isEqualTo("ABC"); 93 | 94 | } 95 | 96 | 97 | @Test 98 | public void parse_THROW_on_notation_violation() throws Exception { 99 | 100 | // Given 101 | 102 | String swiftFieldNotation = "5!a"; 103 | 104 | String fieldText = "ABC"; 105 | 106 | // When 107 | 108 | Throwable thrown = catchThrowable(() -> {new SwiftNotation(swiftFieldNotation).parse(fieldText); }); 109 | 110 | // then 111 | assertThat(thrown).isInstanceOf(FieldNotationParseException.class) 112 | .hasFieldOrPropertyWithValue("index", 0); 113 | 114 | } 115 | 116 | @Test 117 | public void constructor_THROW_on_invalid_notation() throws Exception { 118 | 119 | // Given 120 | 121 | String swiftFieldNotation = "a!1"; 122 | 123 | // When 124 | 125 | Throwable thrown = catchThrowable(() -> {new SwiftNotation(swiftFieldNotation); }); 126 | 127 | // then 128 | assertThat(thrown).isInstanceOf(SwiftNotationParseException.class); 129 | 130 | } 131 | 132 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/SwiftFieldReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage; 2 | 3 | import com.qoomon.banking.TestUtils; 4 | import com.qoomon.banking.swift.submessage.field.GeneralField; 5 | import com.qoomon.banking.swift.submessage.field.SwiftFieldReader; 6 | import com.qoomon.banking.swift.submessage.field.exception.FieldParseException; 7 | import org.assertj.core.api.SoftAssertions; 8 | import org.junit.Test; 9 | 10 | import java.io.StringReader; 11 | import java.util.List; 12 | 13 | import static org.assertj.core.api.Assertions.*; 14 | 15 | /** 16 | * Created by qoomon on 27/06/16. 17 | */ 18 | public class SwiftFieldReaderTest { 19 | 20 | private SoftAssertions softly = new SoftAssertions(); 21 | 22 | @Test 23 | public void readField_WHEN_valid_message_text_THEN_return_fields() throws Exception { 24 | 25 | // Given 26 | String swiftMessage = ":1:fizz\n:2:buzz"; 27 | SwiftFieldReader classUnderTest = new SwiftFieldReader(new StringReader(swiftMessage)); 28 | 29 | // When 30 | List fieldList = TestUtils.collectUntilNull(classUnderTest::readField); 31 | 32 | // Then 33 | assertThat(fieldList).hasSize(2); 34 | softly.assertThat(fieldList.get(0).getTag()).isEqualTo("1"); 35 | softly.assertThat(fieldList.get(0).getContent()).isEqualTo("fizz"); 36 | softly.assertThat(fieldList.get(1).getTag()).isEqualTo("2"); 37 | softly.assertThat(fieldList.get(1).getContent()).isEqualTo("buzz"); 38 | softly.assertAll(); 39 | 40 | } 41 | 42 | 43 | @Test 44 | public void readField_WHEN_detecting_multiline_fields_THEN_return_fields_with_joined_content() throws Exception { 45 | 46 | // Given 47 | String swiftMessage = ":1:fizz\n:2:multi\r\nline"; 48 | SwiftFieldReader classUnderTest = new SwiftFieldReader(new StringReader(swiftMessage)); 49 | 50 | // When 51 | List fieldList = TestUtils.collectUntilNull(classUnderTest::readField); 52 | 53 | // Then 54 | assertThat(fieldList).hasSize(2); 55 | softly.assertThat(fieldList.get(0).getTag()).isEqualTo("1"); 56 | softly.assertThat(fieldList.get(0).getContent()).isEqualTo("fizz"); 57 | softly.assertThat(fieldList.get(1).getTag()).isEqualTo("2"); 58 | softly.assertThat(fieldList.get(1).getContent()).isEqualTo("multi\nline"); 59 | softly.assertAll(); 60 | 61 | } 62 | 63 | @Test 64 | public void readField_WHEN_detecting_content_without_field_tag_THEN_throw_exception() throws Exception { 65 | 66 | // Given 67 | String swiftMessage = "fizz\n:2:buzz"; 68 | SwiftFieldReader classUnderTest = new SwiftFieldReader(new StringReader(swiftMessage)); 69 | 70 | // When 71 | Throwable exception = catchThrowable(() -> TestUtils.collectUntilNull(classUnderTest::readField)); 72 | 73 | // Then 74 | assertThat(exception).as("Exception").isInstanceOf(FieldParseException.class); 75 | 76 | FieldParseException parseException = (FieldParseException) exception; 77 | assertThat(parseException.getLineNumber()).isEqualTo(1); 78 | 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/AccountIdentificationTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.AssertionsForInterfaceTypes.*; 6 | 7 | 8 | /** 9 | * Created by qoomon on 22/07/16. 10 | */ 11 | public class AccountIdentificationTest { 12 | 13 | @Test 14 | public void of() throws Exception { 15 | // Given 16 | 17 | GeneralField generalField = new GeneralField(AccountIdentification.FIELD_TAG_25, "aabbccddeeff112233,"); 18 | 19 | // When 20 | AccountIdentification field = AccountIdentification.of(generalField); 21 | 22 | // Then 23 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 24 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/BCSMessageParserTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.qoomon.banking.swift.bcsmessage.BCSMessage; 4 | import com.qoomon.banking.swift.bcsmessage.BCSMessageParseException; 5 | import com.qoomon.banking.swift.bcsmessage.BCSMessageParser; 6 | import org.junit.Test; 7 | 8 | import static org.assertj.core.api.Assertions.*; 9 | 10 | /** 11 | * Created by qoomon on 25/07/16. 12 | */ 13 | public class BCSMessageParserTest { 14 | 15 | @Test 16 | public void parse_SHOULD_parse_palid_message() throws Exception { 17 | // Given 18 | String messageText = "835?20foo?36bar"; 19 | 20 | BCSMessageParser subjectUnderTest = new BCSMessageParser(); 21 | 22 | // When 23 | BCSMessage message = subjectUnderTest.parseMessage(messageText); 24 | 25 | // Then 26 | assertThat(message).isNotNull(); 27 | assertThat(message.getBusinessTransactionCode()).isEqualTo("835"); 28 | assertThat(message.getFieldMap()) 29 | .containsEntry("20", "foo") 30 | .containsEntry("36", "bar") 31 | .hasSize(2); 32 | } 33 | 34 | 35 | @Test 36 | public void parse_SHOULD_accept_any_delimiter() throws Exception { 37 | // Given 38 | String messageText = "835/20foo/36bar"; 39 | 40 | BCSMessageParser subjectUnderTest = new BCSMessageParser(); 41 | 42 | // When 43 | BCSMessage message = subjectUnderTest.parseMessage(messageText); 44 | 45 | // Then 46 | assertThat(message).isNotNull(); 47 | assertThat(message.getBusinessTransactionCode()).isEqualTo("835"); 48 | assertThat(message.getFieldMap()) 49 | .containsEntry("20", "foo") 50 | .containsEntry("36", "bar") 51 | .hasSize(2); 52 | } 53 | 54 | @Test 55 | public void parse_THROW_on_duplicate_fields() throws Exception { 56 | // Given 57 | String messageText = "835?20foo?20bar"; 58 | 59 | BCSMessageParser subjectUnderTest = new BCSMessageParser(); 60 | 61 | // When 62 | 63 | Throwable thrown = catchThrowable(() -> subjectUnderTest.parseMessage(messageText)); 64 | 65 | // then 66 | assertThat(thrown).isInstanceOf(BCSMessageParseException.class) 67 | .hasMessageContaining("duplicate field " + "20"); 68 | } 69 | 70 | 71 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/ClosingAvailableBalanceTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 4 | import org.assertj.core.api.Assertions; 5 | import org.joda.money.BigMoney; 6 | import org.joda.money.CurrencyUnit; 7 | import org.junit.Test; 8 | 9 | import java.math.BigDecimal; 10 | import java.time.LocalDate; 11 | 12 | import static org.assertj.core.api.AssertionsForInterfaceTypes.*; 13 | 14 | 15 | /** 16 | * Created by qoomon on 22/07/16. 17 | */ 18 | public class ClosingAvailableBalanceTest { 19 | 20 | 21 | @Test 22 | public void of() throws Exception { 23 | // Given 24 | GeneralField generalField = new GeneralField(ClosingAvailableBalance.FIELD_TAG_64, "D160717EUR2233,"); 25 | 26 | // When 27 | ClosingAvailableBalance field = ClosingAvailableBalance.of(generalField); 28 | 29 | // Then 30 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 31 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 32 | } 33 | 34 | @Test 35 | public void getSignedAmount_WHEN_debit_transaction_THEN_return_negative_amount() throws Exception { 36 | // Given 37 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 38 | ClosingAvailableBalance classUnderTest = new ClosingAvailableBalance(LocalDate.now(), 39 | DebitCreditMark.DEBIT, 40 | amount); 41 | 42 | // When 43 | BigMoney signedAmount = classUnderTest.getSignedAmount(); 44 | 45 | // Then 46 | assertThat(signedAmount).isEqualTo(amount.negated()); 47 | } 48 | 49 | @Test 50 | public void getSignedAmount_WHEN_credit_transaction_THEN_return_positive_amount() throws Exception { 51 | // Given 52 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 53 | ClosingAvailableBalance classUnderTest = new ClosingAvailableBalance(LocalDate.now(), 54 | DebitCreditMark.CREDIT, 55 | amount); 56 | 57 | // When 58 | BigMoney signedAmount = classUnderTest.getSignedAmount(); 59 | 60 | // Then 61 | assertThat(signedAmount).isEqualTo(amount); 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/ClosingBalanceTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 4 | import org.assertj.core.api.Assertions; 5 | import org.joda.money.BigMoney; 6 | import org.joda.money.CurrencyUnit; 7 | import org.junit.Test; 8 | 9 | import java.time.LocalDate; 10 | 11 | import static org.assertj.core.api.Assertions.*; 12 | 13 | /** 14 | * Created by qoomon on 21/07/16. 15 | */ 16 | public class ClosingBalanceTest { 17 | 18 | @Test 19 | public void of() throws Exception { 20 | // Given 21 | 22 | GeneralField generalField = new GeneralField(ClosingBalance.FIELD_TAG_62F, "D160717EUR123,"); 23 | 24 | // When 25 | ClosingBalance field = ClosingBalance.of(generalField); 26 | 27 | // Then 28 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 29 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 30 | } 31 | 32 | @Test 33 | public void getSignedAmount_WHEN_debit_transaction_THEN_return_negative_amount() throws Exception { 34 | // Given 35 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 36 | ClosingBalance classUnderTest = new ClosingBalance(ClosingBalance.Type.CLOSING, LocalDate.now(), 37 | DebitCreditMark.DEBIT, 38 | amount); 39 | 40 | // When 41 | BigMoney signedAmount = classUnderTest.getSignedAmount(); 42 | 43 | // Then 44 | assertThat(signedAmount).isEqualTo(amount.negated()); 45 | } 46 | 47 | @Test 48 | public void getSignedAmount_WHEN_credit_transaction_THEN_return_positive_amount() throws Exception { 49 | // Given 50 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 51 | ClosingBalance classUnderTest = new ClosingBalance(ClosingBalance.Type.CLOSING, LocalDate.now(), 52 | DebitCreditMark.CREDIT, 53 | amount); 54 | 55 | // When 56 | BigMoney signedAmount = classUnderTest.getSignedAmount(); 57 | 58 | // Then 59 | assertThat(signedAmount).isEqualTo(amount); 60 | } 61 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/DateTimeIndicatorTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | 4 | import org.junit.Test; 5 | 6 | import static org.assertj.core.api.AssertionsForClassTypes.*; 7 | 8 | /** 9 | * Created by qoomon on 22/07/16. 10 | */ 11 | public class DateTimeIndicatorTest { 12 | 13 | @Test 14 | public void of() throws Exception { 15 | // Given 16 | GeneralField generalField = new GeneralField(DateTimeIndicator.FIELD_TAG_13D, "1607171122-0130"); 17 | 18 | // When 19 | DateTimeIndicator field = DateTimeIndicator.of(generalField); 20 | 21 | // Then 22 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 23 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/FloorLimitIndicatorTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 4 | import org.assertj.core.api.Assertions; 5 | import org.joda.money.BigMoney; 6 | import org.joda.money.CurrencyUnit; 7 | import org.junit.Test; 8 | 9 | import java.util.Optional; 10 | 11 | import static org.assertj.core.api.Assertions.*; 12 | 13 | /** 14 | * Created by qoomon on 22/08/16. 15 | */ 16 | public class FloorLimitIndicatorTest { 17 | 18 | @Test 19 | public void of() throws Exception { 20 | // Given 21 | 22 | GeneralField generalField = new GeneralField(FloorLimitIndicator.FIELD_TAG_34F, "EUR123,"); 23 | 24 | // When 25 | FloorLimitIndicator field = FloorLimitIndicator.of(generalField); 26 | 27 | // Then 28 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 29 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 30 | } 31 | 32 | @Test 33 | public void getSignedAmount_SHOULD_not_be_present_if_credit_debit_mark_ist_set() throws Exception { 34 | // Given 35 | DebitCreditMark debitCreditMark = null; 36 | FloorLimitIndicator classUnderTest = new FloorLimitIndicator( 37 | debitCreditMark, 38 | BigMoney.zero(CurrencyUnit.EUR)); 39 | 40 | // When 41 | Optional signedAmount = classUnderTest.getSignedAmount(); 42 | 43 | // Then 44 | assertThat(signedAmount).isNotPresent(); 45 | } 46 | 47 | @Test 48 | public void getSignedAmount_WHEN_debit_transaction_THEN_return_negative_amount() throws Exception { 49 | // Given 50 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 51 | FloorLimitIndicator classUnderTest = new FloorLimitIndicator( 52 | DebitCreditMark.DEBIT, 53 | amount); 54 | 55 | // When 56 | Optional signedAmount = classUnderTest.getSignedAmount(); 57 | 58 | // Then 59 | assertThat(signedAmount).contains(amount.negated()); 60 | } 61 | 62 | @Test 63 | public void getSignedAmount_WHEN_credit_transaction_THEN_return_positive_amount() throws Exception { 64 | // Given 65 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 66 | FloorLimitIndicator classUnderTest = new FloorLimitIndicator( 67 | DebitCreditMark.CREDIT, 68 | amount); 69 | 70 | // When 71 | Optional signedAmount = classUnderTest.getSignedAmount(); 72 | 73 | // Then 74 | assertThat(signedAmount).contains(amount); 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/ForwardAvailableBalanceTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 4 | import org.assertj.core.api.Assertions; 5 | import org.joda.money.BigMoney; 6 | import org.joda.money.CurrencyUnit; 7 | import org.junit.Test; 8 | 9 | import java.time.LocalDate; 10 | 11 | import static org.assertj.core.api.Assertions.*; 12 | 13 | /** 14 | * Created by qoomon on 18/07/2016. 15 | */ 16 | public class ForwardAvailableBalanceTest { 17 | 18 | @Test 19 | public void of() throws Exception { 20 | // Given 21 | GeneralField generalField = new GeneralField(ForwardAvailableBalance.FIELD_TAG_65, "D" + "160130" + "EUR" + "123,456"); 22 | 23 | // When 24 | ForwardAvailableBalance field = ForwardAvailableBalance.of(generalField); 25 | 26 | // Then 27 | assertThat(field).isNotNull(); 28 | assertThat(field.getDebitCreditMark()).isEqualTo(DebitCreditMark.DEBIT); 29 | assertThat(field.getEntryDate()).isEqualTo(LocalDate.of(2016, 1, 30)); 30 | assertThat(field.getAmount()).isEqualByComparingTo(BigMoney.of(CurrencyUnit.EUR, 123.456)); 31 | 32 | } 33 | 34 | @Test 35 | public void getSignedAmount_WHEN_debit_transaction_THEN_return_negative_amount() throws Exception { 36 | // Given 37 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 38 | ForwardAvailableBalance classUnderTest = new ForwardAvailableBalance(LocalDate.now(), 39 | DebitCreditMark.DEBIT, 40 | amount); 41 | 42 | // When 43 | BigMoney signedAmount = classUnderTest.getSignedAmount(); 44 | 45 | // Then 46 | assertThat(signedAmount).isEqualTo(amount.negated()); 47 | } 48 | 49 | @Test 50 | public void getSignedAmount_WHEN_credit_transaction_THEN_return_positive_amount() throws Exception { 51 | // Given 52 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 53 | ForwardAvailableBalance classUnderTest = new ForwardAvailableBalance(LocalDate.now(), 54 | DebitCreditMark.CREDIT, 55 | amount); 56 | 57 | // When 58 | BigMoney signedAmount = classUnderTest.getSignedAmount(); 59 | 60 | // Then 61 | assertThat(signedAmount).isEqualTo(amount); 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/InformationToAccountOwnerTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.AssertionsForInterfaceTypes.*; 6 | 7 | /** 8 | * Created by qoomon on 22/07/16. 9 | */ 10 | public class InformationToAccountOwnerTest { 11 | 12 | @Test 13 | public void of() throws Exception { 14 | // Given 15 | GeneralField generalField = new GeneralField(InformationToAccountOwner.FIELD_TAG_86, "1607171122-0130,"); 16 | 17 | // When 18 | InformationToAccountOwner field = InformationToAccountOwner.of(generalField); 19 | 20 | // Then 21 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 22 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/OpeningBalanceTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 4 | import org.joda.money.BigMoney; 5 | import org.joda.money.CurrencyUnit; 6 | import org.junit.Test; 7 | 8 | import java.time.LocalDate; 9 | 10 | import static org.assertj.core.api.Assertions.*; 11 | 12 | /** 13 | * Created by qoomon on 22/07/16. 14 | */ 15 | public class OpeningBalanceTest { 16 | 17 | @Test 18 | public void of() throws Exception { 19 | // Given 20 | GeneralField generalField = new GeneralField(OpeningBalance.FIELD_TAG_60F, "D160717EUR123,"); 21 | 22 | // When 23 | OpeningBalance field = OpeningBalance.of(generalField); 24 | 25 | // Then 26 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 27 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 28 | } 29 | 30 | @Test 31 | public void getSignedAmount_WHEN_debit_transaction_THEN_return_negative_amount() throws Exception { 32 | // Given 33 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 34 | OpeningBalance classUnderTest = new OpeningBalance(OpeningBalance.Type.OPENING, LocalDate.now(), 35 | DebitCreditMark.DEBIT, 36 | amount); 37 | 38 | // When 39 | BigMoney signedAmount = classUnderTest.getSignedAmount(); 40 | 41 | // Then 42 | assertThat(signedAmount).isEqualTo(amount.negated()); 43 | } 44 | 45 | @Test 46 | public void getSignedAmount_WHEN_credit_transaction_THEN_return_positive_amount() throws Exception { 47 | // Given 48 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 49 | OpeningBalance classUnderTest = new OpeningBalance(OpeningBalance.Type.OPENING, LocalDate.now(), 50 | DebitCreditMark.CREDIT, 51 | amount); 52 | 53 | // When 54 | BigMoney signedAmount = classUnderTest.getSignedAmount(); 55 | 56 | // Then 57 | assertThat(signedAmount).isEqualTo(amount); 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/RelatedReferenceTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.AssertionsForClassTypes.*; 6 | 7 | /** 8 | * Created by qoomon on 22/07/16. 9 | */ 10 | public class RelatedReferenceTest { 11 | 12 | @Test 13 | public void of() throws Exception { 14 | // Given 15 | GeneralField generalField = new GeneralField(RelatedReference.FIELD_TAG_21, "1607171122-0130,"); 16 | 17 | // When 18 | RelatedReference field = RelatedReference.of(generalField); 19 | 20 | // Then 21 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 22 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/StatementLineTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 4 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditType; 5 | import com.qoomon.banking.swift.submessage.field.subfield.TransactionTypeIdentificationCode; 6 | import org.assertj.core.api.Assertions; 7 | import org.junit.Test; 8 | 9 | import java.math.BigDecimal; 10 | import java.time.LocalDate; 11 | 12 | import static org.assertj.core.api.AssertionsForClassTypes.*; 13 | 14 | /** 15 | * Created by qoomon on 18/07/2016. 16 | */ 17 | public class StatementLineTest { 18 | 19 | @Test 20 | public void of() throws Exception { 21 | // Given 22 | GeneralField generalField = new GeneralField(StatementLine.FIELD_TAG_61, "160130" + "C" + "R" + "123,456" + "NSTO" + "abcdef" + "//xyz" + "\nfoobar"); 23 | 24 | // When 25 | StatementLine field = StatementLine.of(generalField); 26 | 27 | // Then 28 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 29 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 30 | assertThat(field.getDebitCreditType()).isEqualTo(DebitCreditType.REGULAR); 31 | assertThat(field.getSignedAmount()).isEqualTo(new BigDecimal("123.456")); 32 | } 33 | 34 | @Test 35 | public void getSignedAmount_WHEN_regular_debit_transaction_THEN_return_negative_amount() throws Exception { 36 | // Given 37 | BigDecimal amount = BigDecimal.ONE; 38 | StatementLine classUnderTest = new StatementLine(LocalDate.now(), 39 | LocalDate.now(), 40 | DebitCreditType.REGULAR, 41 | DebitCreditMark.DEBIT, 42 | amount, 43 | null, 44 | TransactionTypeIdentificationCode.of("NWAR"), 45 | "123456", 46 | null, 47 | null); 48 | 49 | // When 50 | BigDecimal signedAmount = classUnderTest.getSignedAmount(); 51 | 52 | // Then 53 | Assertions.assertThat(signedAmount).isEqualTo(amount.negate()); 54 | } 55 | 56 | @Test 57 | public void getSignedAmount_WHEN_regular_credit_transaction_THEN_return_positive_amount() throws Exception { 58 | // Given 59 | BigDecimal amount = BigDecimal.ONE; 60 | StatementLine classUnderTest = new StatementLine(LocalDate.now(), 61 | LocalDate.now(), 62 | DebitCreditType.REGULAR, 63 | DebitCreditMark.CREDIT, 64 | amount, 65 | null, 66 | TransactionTypeIdentificationCode.of("NWAR"), 67 | "123456", 68 | null, 69 | null); 70 | 71 | // When 72 | BigDecimal signedAmount = classUnderTest.getSignedAmount(); 73 | 74 | // Then 75 | Assertions.assertThat(signedAmount).isEqualTo(amount); 76 | } 77 | 78 | @Test 79 | public void getSignedAmount_WHEN_reversal_debit_transaction_THEN_return_positive_amount() throws Exception { 80 | // Given 81 | BigDecimal amount = BigDecimal.ONE; 82 | StatementLine classUnderTest = new StatementLine(LocalDate.now(), 83 | LocalDate.now(), 84 | DebitCreditType.REVERSAL, 85 | DebitCreditMark.DEBIT, 86 | amount, 87 | null, 88 | TransactionTypeIdentificationCode.of("NWAR"), 89 | "123456", 90 | null, 91 | null); 92 | 93 | // When 94 | BigDecimal signedAmount = classUnderTest.getSignedAmount(); 95 | 96 | // Then 97 | Assertions.assertThat(signedAmount).isEqualTo(amount); 98 | } 99 | 100 | @Test 101 | public void getSignedAmount_WHEN_reversal_credit_transaction_THEN_return_negative_amount() throws Exception { 102 | // Given 103 | BigDecimal amount = BigDecimal.ONE; 104 | StatementLine classUnderTest = new StatementLine(LocalDate.now(), 105 | LocalDate.now(), 106 | DebitCreditType.REVERSAL, 107 | DebitCreditMark.CREDIT, 108 | amount, 109 | null, 110 | TransactionTypeIdentificationCode.of("NWAR"), 111 | "123456", 112 | null, 113 | null); 114 | 115 | // When 116 | BigDecimal signedAmount = classUnderTest.getSignedAmount(); 117 | 118 | // Then 119 | Assertions.assertThat(signedAmount).isEqualTo(amount.negate()); 120 | } 121 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/StatementNumberTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.Assertions.*; 6 | 7 | /** 8 | * Created by qoomon on 20/07/16. 9 | */ 10 | public class StatementNumberTest { 11 | 12 | 13 | @Test 14 | public void of() throws Exception { 15 | // Given 16 | 17 | GeneralField generalField = new GeneralField(StatementNumber.FIELD_TAG_28C, "12345/67890"); 18 | 19 | // When 20 | StatementNumber field = StatementNumber.of(generalField); 21 | 22 | // Then 23 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 24 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/TransactionReferenceNumberTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.AssertionsForInterfaceTypes.*; 6 | 7 | /** 8 | * Created by qoomon on 22/07/16. 9 | */ 10 | public class TransactionReferenceNumberTest { 11 | 12 | @Test 13 | public void of() throws Exception { 14 | // Given 15 | GeneralField generalField = new GeneralField(TransactionReferenceNumber.FIELD_TAG_20, "1234567-0130,"); 16 | 17 | // When 18 | TransactionReferenceNumber field = TransactionReferenceNumber.of(generalField); 19 | 20 | // Then 21 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 22 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/field/TransactionSummaryTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.field; 2 | 3 | import com.qoomon.banking.swift.submessage.field.subfield.DebitCreditMark; 4 | import org.joda.money.BigMoney; 5 | import org.joda.money.CurrencyUnit; 6 | import org.junit.Test; 7 | 8 | import static org.assertj.core.api.AssertionsForClassTypes.*; 9 | 10 | /** 11 | * Created by qoomon on 22/07/16. 12 | */ 13 | public class TransactionSummaryTest { 14 | 15 | @Test 16 | public void of() throws Exception { 17 | // Given 18 | GeneralField generalField = new GeneralField(TransactionSummary.FIELD_TAG_90C, "12EUR123,"); 19 | 20 | // When 21 | TransactionSummary field = TransactionSummary.of(generalField); 22 | 23 | // Then 24 | assertThat(field.getTag()).isEqualTo(generalField.getTag()); 25 | assertThat(field.getContent()).isEqualTo(generalField.getContent()); 26 | } 27 | 28 | @Test 29 | public void getSignedAmount_WHEN_debit_transaction_THEN_return_negative_amount() throws Exception { 30 | // Given 31 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 32 | TransactionSummary classUnderTest = new TransactionSummary( 33 | DebitCreditMark.DEBIT, 34 | 1, 35 | amount); 36 | 37 | // When 38 | BigMoney signedAmount = classUnderTest.getSignedAmount(); 39 | 40 | // Then 41 | assertThat(signedAmount).isEqualTo(amount.negated()); 42 | } 43 | 44 | @Test 45 | public void getSignedAmount_WHEN_credit_transaction_THEN_return_positive_amount() throws Exception { 46 | // Given 47 | BigMoney amount = BigMoney.of(CurrencyUnit.EUR, 1); 48 | TransactionSummary classUnderTest = new TransactionSummary( 49 | DebitCreditMark.CREDIT, 50 | 1, 51 | amount); 52 | 53 | // When 54 | BigMoney signedAmount = classUnderTest.getSignedAmount(); 55 | 56 | // Then 57 | assertThat(signedAmount).isEqualTo(amount); 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/test/java/com/qoomon/banking/swift/submessage/mt940/MT940PageReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.qoomon.banking.swift.submessage.mt940; 2 | 3 | import com.google.common.base.Throwables; 4 | import com.google.common.io.Resources; 5 | import com.qoomon.banking.TestUtils; 6 | import com.qoomon.banking.swift.message.exception.SwiftMessageParseException; 7 | import org.assertj.core.api.SoftAssertions; 8 | import org.junit.Test; 9 | 10 | import java.io.FileReader; 11 | import java.io.StringReader; 12 | import java.net.URL; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.util.List; 17 | import java.util.stream.Stream; 18 | 19 | import static org.assertj.core.api.Assertions.*; 20 | 21 | 22 | /** 23 | * Created by qoomon on 27/06/16. 24 | */ 25 | public class MT940PageReaderTest { 26 | 27 | 28 | @Test 29 | public void parse_WHEN_parse_valid_file_RETURN_message() throws Exception { 30 | 31 | // Given 32 | String mt940MessageText = ":20:02618\n" + 33 | ":21:123456/DEV\n" + 34 | ":25:6-9412771\n" + 35 | ":28C:00102\n" + 36 | ":60F:C000103USD672,\n" + 37 | ":61:0312091211D880,FTRFBPHP/081203/0003//59512112915002\n" + 38 | "supplementary info\n" + 39 | ":86:multiline info\n" + 40 | "-info\n" + 41 | ":61:0312091211D880,FTRFBPHP/081203/0003//59512112915002\n" + 42 | ":86:singleline info\n" + 43 | ":61:0312091211D880,FTRFBPHP/081203/0003//59512112915002\n" + 44 | ":62F:C000103USD987,\n" + 45 | ":86:multiline summary\n" + 46 | "summary\n" + 47 | "-"; 48 | 49 | MT940PageReader classUnderTest = new MT940PageReader(new StringReader(mt940MessageText)); 50 | 51 | // When 52 | List pageList = TestUtils.collectUntilNull(classUnderTest::read); 53 | 54 | // Then 55 | assertThat(pageList).hasSize(1); 56 | MT940Page MT940Page = pageList.get(0); 57 | SoftAssertions softly = new SoftAssertions(); 58 | softly.assertThat(MT940Page.getTransactionGroupList()).hasSize(3); 59 | softly.assertThat(MT940Page.getTransactionGroupList()).hasSize(3); 60 | } 61 | 62 | @Test 63 | public void parse_WHEN_supplementary_details_start_with_colon_THEN_it_is_correctly_parsed() throws Exception { 64 | 65 | // Given 66 | String mt940MessageText = ":20:02618\n" + 67 | ":21:123456/DEV\n" + 68 | ":25:6-9412771\n" + 69 | ":28C:00102\n" + 70 | ":60F:C000103USD672,\n" + 71 | ":61:0312091211D880,FTRFBPHP/081203/0003//59512112915002\n" + 72 | ":colon is valid x char set\n" + 73 | ":62F:C000103USD987,\n" + 74 | "-"; 75 | 76 | MT940PageReader classUnderTest = new MT940PageReader(new StringReader(mt940MessageText)); 77 | 78 | // When 79 | List pageList = TestUtils.collectUntilNull(classUnderTest::read); 80 | 81 | // Then 82 | assertThat(pageList).hasSize(1); 83 | } 84 | 85 | @Test 86 | public void getContent_SHOULD_return_input_text() throws Exception { 87 | 88 | // Given 89 | String contentInput = ":20:02618\n" + 90 | ":21:123456/DEV\n" + 91 | ":25:6-9412771\n" + 92 | ":28C:00102\n" + 93 | ":60F:C000103USD672,\n" + 94 | ":61:0312091211D880,FTRFBPHP/081203/0003//59512112915002\n" + 95 | ":86:multiline info\n" + 96 | "-info\n" + 97 | ":61:0312091211D880,FTRFBPHP/081203/0003//59512112915002\n" + 98 | ":86:singleline info\n" + 99 | ":61:0312091211D880,FTRFBPHP/081203/0003//59512112915002\n" + 100 | ":62F:C000103USD987,\n" + 101 | ":86:multiline summary\n" + 102 | "summary\n" + 103 | "-"; 104 | MT940PageReader pageReader = new MT940PageReader(new StringReader(contentInput)); 105 | MT940Page classUnderTest = TestUtils.collectUntilNull(pageReader::read).get(0); 106 | 107 | // When 108 | String content = classUnderTest.getContent(); 109 | 110 | // Then 111 | assertThat(content).isEqualTo(contentInput); 112 | } 113 | 114 | @Test 115 | public void parse_WHEN_funds_code_does_not_match_statement_currency_THROW_exception() throws Exception { 116 | 117 | // Given 118 | String mt940MessageText = ":20:02618\n" + 119 | ":21:123456/DEV\n" + 120 | ":25:6-9412771\n" + 121 | ":28C:00102\n" + 122 | ":60F:C000103USD672,\n" + // currency USD 123 | ":61:0312091211DX880,FTRFBPHP/081203/0003//59512112915002\n" + // wrong funds code X expect usD 124 | ":62F:C000103USD987,\n" + 125 | "-"; 126 | 127 | MT940PageReader classUnderTest = new MT940PageReader(new StringReader(mt940MessageText)); 128 | 129 | // When 130 | Throwable exception = catchThrowable(classUnderTest::read); 131 | 132 | // Then 133 | assertThat(exception).isInstanceOf(SwiftMessageParseException.class).hasRootCauseInstanceOf(IllegalArgumentException.class); 134 | } 135 | 136 | @Test 137 | public void parse_WHEN_unfinished_page_detected_THROW_exception() throws Exception { 138 | 139 | // Given 140 | String mt940MessageText = ":20:02618\n"; 141 | 142 | MT940PageReader classUnderTest = new MT940PageReader(new StringReader(mt940MessageText)); 143 | 144 | // When 145 | Throwable exception = catchThrowable(classUnderTest::read); 146 | 147 | // Then 148 | assertThat(exception).isInstanceOf(SwiftMessageParseException.class); 149 | 150 | } 151 | 152 | @Test 153 | public void parse_WHEN_parse_many_valid_file_RETURN_message() throws Exception { 154 | 155 | // Given 156 | URL mt940_valid_folder = Resources.getResource("submessage/mt940_valid"); 157 | Stream files = Files.walk(Paths.get(mt940_valid_folder.toURI())).filter(path -> Files.isRegularFile(path)); 158 | 159 | // When 160 | final int[] errors = {0}; 161 | files.forEach(filePath -> { 162 | try { 163 | MT940PageReader classUnderTest = new MT940PageReader(new FileReader(filePath.toFile())); 164 | List messageList = TestUtils.collectUntilNull(classUnderTest::read); 165 | assertThat(messageList).isNotEmpty(); 166 | } catch (Exception e) { 167 | System.out.println(filePath); 168 | System.out.println(Throwables.getStackTraceAsString(e)); 169 | System.out.println(); 170 | errors[0]++; 171 | } 172 | }); 173 | 174 | // Then 175 | assertThat(errors[0]).isEqualTo(0); 176 | 177 | } 178 | } -------------------------------------------------------------------------------- /src/test/resources/submessage/mt940_valid/valid-mt940-content.txt: -------------------------------------------------------------------------------- 1 | :20:02618 2 | :21:123456/DEV 3 | :25:6-9412771 4 | :28C:00102 5 | :60F:C000103USD672, 6 | :61:0312091209D880,FTRFBPHP/081203/0003//59512092915002 7 | :86:multiline info 8 | -info 9 | :61:0312091209D880,FTRFBPHP/081203/0003//59512092915002 10 | :86:singleline info 11 | :61:0312091209D880,FTRFBPHP/081203/0003//59512092915002 12 | :62F:C000103USD987, 13 | :86:multiline summary 14 | summary 15 | - -------------------------------------------------------------------------------- /src/test/resources/submessage/mt942_valid/valid-mt942-content.txt: -------------------------------------------------------------------------------- 1 | :20:02761 2 | :25:6-9412771 3 | :28C:1/1 4 | :34F:USD123, 5 | :13D:0001032359+0500 6 | :61:0312091209D880,FTRFBPHP/081203/0003//59512092915002 7 | :86:multiline info 8 | -info 9 | :61:0312091209D880,FTRFBPHP/081203/0003//59512092915002 10 | :86:singleline info 11 | :61:0312091209D880,FTRFBPHP/081203/0003//59512092915002 12 | :90D:75475USD123, 13 | :90C:75475USD123, 14 | :86:multiline summary 15 | summary 16 | - -------------------------------------------------------------------------------- /src/test/resources/swiftmessage/valid-mt940.txt: -------------------------------------------------------------------------------- 1 | {1:F01COPZBEB0AXXX0377002460}{2:O9401506110804LRLRXXXX4A1100009040831108041707N}{3:{108:MT940 003 OF 058}}{4: 2 | :20:02618 3 | :21:123456/DEV 4 | :25:6-9412771 5 | :28C:00102 6 | :60F:C000103USD672, 7 | :61:0312091209D880,FTRFREF:BPHPBK/081203/0003//59512092915002 8 | :86:multiline info 9 | -info 10 | :61:0312091209D880,FTRFREF:BPHPBK/081203/0003//59512092915002 11 | :86:singleline info 12 | :61:0312091209D880,FTRFREF:BPHPBK/081203/0003//59512092915002 13 | :62F:C000103USD987, 14 | :86:multiline summary 15 | summary 16 | -}{5:{CHK:592A3DB2CA5B}{TNG:}}{S:{COP:P}} -------------------------------------------------------------------------------- /src/test/resources/swiftmessage/valid-mt942.txt: -------------------------------------------------------------------------------- 1 | {1:F01SIIBUS30AXXX0481250571}{2:O9421332080620LPLPXXXX4A0800001263160806200933N}{3:{108:MT942 005 OF 015}}{4: 2 | :20:02761 3 | :25:6-9412771 4 | :28C:1/1 5 | :34F:USD123, 6 | :13D:0001032359+0500 7 | :61:0312091209D880,FTRFREF:BPHPBK/081203/0003//59512092915002 8 | :86:multiline info 9 | -info 10 | :61:0312091209D880,FTRFREF:BPHPBK/081203/0003//59512092915002 11 | :86:singleline info 12 | :61:0312091209D880,FTRFREF:BPHPBK/081203/0003//59512092915002 13 | :90D:75475USD123, 14 | :90C:75475USD123, 15 | :86:multiline summary 16 | smumary 17 | -}{5:{CHK:F7C4F89AF66D}{TNG:}}{S:{COP:P}} --------------------------------------------------------------------------------