diff --git a/CHANGELOG.md b/CHANGELOG.md index d06162829..b210504d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Prowide ISO 20022 - CHANGELOG +#### 9.4.8 - SNAPSHOT + * (PW-2113) Added API in the `MxParseUtils` to extract comments from XML string + * (PW-2113) Added API in the `MxParseUtils` to extract the enclosed MT from a multi-format MX message + #### 9.4.7 - August 2024 * (PW-1958) Fixed the `DefaultMxMetadataStrategy` NPE issue when the amount values are null diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxParseUtils.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxParseUtils.java index c9820b43a..09047718f 100644 --- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxParseUtils.java +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxParseUtils.java @@ -18,14 +18,22 @@ import com.prowidesoftware.ProwideException; import com.prowidesoftware.swift.model.DistinguishedName; import com.prowidesoftware.swift.model.MxId; +import com.prowidesoftware.swift.model.mt.AbstractMT; import com.prowidesoftware.swift.utils.SafeXmlUtils; +import java.io.IOException; import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.xml.bind.*; import javax.xml.bind.annotation.adapters.XmlAdapter; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.transform.sax.SAXSource; @@ -241,4 +249,120 @@ private static Optional enrichBusinessService(MxId mxId, final String xml) public static String makeXmlLenient(String xml) { return xml != null ? xml.replaceFirst("(?i)<\\?XML", "This method uses an {@link XMLStreamReader} to parse the provided XML string + * and extract all comments present in the document. Comments are identified as + * XML elements of type {@link XMLStreamConstants#COMMENT}. + *

+ * All extracted comments are trimmed before being added to the result list. Meaning they will not contain any + * leading or trailing whitespace. + * + * @param xml the XML document as a {@link String} to parse + * @return a {@link List} of comments extracted from the XML document + * @throws NullPointerException if the {@code xml} is null + * @throws IllegalArgumentException if the {@code xml} is blank or empty + * + * @since 9.4.8 + */ + public static List parseComments(final String xml) { + Objects.requireNonNull(xml, "XML to parse must not be null"); + Validate.notBlank(xml, "XML to parse must not be a blank string"); + + List result = new ArrayList<>(); + + final XMLInputFactory factory = SafeXmlUtils.inputFactory(); + try { + XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(MxParseUtils.makeXmlLenient(xml))); + + while (reader.hasNext()) { + int event = reader.next(); + if (event == XMLStreamConstants.COMMENT) { + String comment = reader.getText(); + if (comment != null) { + result.add(comment.trim()); + } + } + } + reader.close(); + } catch (XMLStreamException e) { + log.log(Level.WARNING, "Error parsing XML comments", e); + } + return result; + } + + /** + * Parses comments from the given XML document that start with a specific prefix. + * + *

This method uses {@link #parseComments(String)} to extract all comments + * from the XML, filters the comments to include only those that start with the + * specified prefix. + * + * @param xml the XML document as a {@link String} to parse + * @param startWith the prefix to filter comments by, leading whitespaces are ignored + * @return a {@link List} of filtered and cropped comments that start with the given prefix + * @throws NullPointerException if the {@code xml} is null + * @throws IllegalArgumentException if the {@code xml} is blank or empty + * + * @since 9.4.8 + */ + public static List parseCommentsStartsWith(final String xml, final String startWith) { + return parseComments(xml).stream() + .filter(c -> c.startsWith(startWith)) // Filter comments that start with the given prefix + .collect(Collectors.toList()); + } + + /** + * Parses comments from the given XML document that contains a specific string. + * + *

This method uses {@link #parseComments(String)} to extract all comments + * from the XML, filters the comments to include only those that contains a specific string. + * + * @param xml the XML document as a {@link String} to parse + * @param contains the content to filter comments by + * @return a {@link List} of filtered and cropped comments that start with the given prefix + * @throws NullPointerException if the {@code xml} is null + * @throws IllegalArgumentException if the {@code xml} is blank or empty + * + * @since 9.4.8 + */ + public static List parseCommentsContains(final String xml, final String contains) { + return parseComments(xml).stream() + .filter(c -> c.contains(contains)) // Filter comments that start with the given prefix + .collect(Collectors.toList()); + } + + /** + * Parses an {@link AbstractMT} message from a multi-format XML message. + * + *

This method searches for MT (Message Type) content within the comments + * of the provided XML document. Specifically, it extracts comments that start + * with the prefix "{1:F0", which indicates the presence of an MT message, and + * attempts to parse the first matching comment into an {@link AbstractMT} object.

+ * + *

If an error occurs during parsing or no matching comments are found, the method + * returns an empty {@link Optional}.

+ * + * @param xml the XML document as a {@link String} containing the multi-format message + * @return an {@link Optional} containing the parsed {@link AbstractMT} if successful; + * otherwise, an empty {@link Optional} + * @throws NullPointerException if the {@code xml} is null + * + * @since 9.4.8 + */ + public static Optional parseMtFromMultiformatMessage(final String xml) { + List MTs = MxParseUtils.parseCommentsStartsWith(xml, "{1:F0"); + if (!MTs.isEmpty()) { + String s = MTs.get(0).replace("^~", "\n"); + + try { + return Optional.of(AbstractMT.parse(s)); + } catch (IOException e) { + log.log(Level.WARNING, "Error extracting AbstractMT from Mx", e); + } + } + return Optional.empty(); + } } diff --git a/iso20022-core/src/test/java/com/prowidesoftware/swift/model/mx/MxParseUtilsTest.java b/iso20022-core/src/test/java/com/prowidesoftware/swift/model/mx/MxParseUtilsTest.java index 239e524f3..47f37d7a4 100644 --- a/iso20022-core/src/test/java/com/prowidesoftware/swift/model/mx/MxParseUtilsTest.java +++ b/iso20022-core/src/test/java/com/prowidesoftware/swift/model/mx/MxParseUtilsTest.java @@ -19,11 +19,254 @@ import com.prowidesoftware.swift.model.MxBusinessProcess; import com.prowidesoftware.swift.model.MxId; +import com.prowidesoftware.swift.model.mt.AbstractMT; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Test; public class MxParseUtilsTest { + final String xml = "" + + "" + + " 2.0.10" + + " " + + " " + + " OYYYXXX33XXX009$2411012329271" + + " pacs.009.001.08" + + " MX" + + " Output" + + " " + + " ou=xxx,o=yyyxxx33,o=swift" + + " " + + " YYYXXX33XXX" + + " xxx" + + " " + + " " + + " " + + " ou=xxx,o=mtuzzz33,o=swift" + + " " + + " MTUZZZ33XXX" + + " xxx" + + " " + + " " + + " " + + " SWIFTNetInterface" + + " Original" + + " Financial" + + " 18DXXXF0FFDC7548" + + " mp/mx/_ImGiIFKDEeyXX57HMRFmxw" + + " mp/mx/_ImGiIFKDEeyXX57HMRFmxw/_XC3TJV9CEeyfdsiuljUhuQ" + + " " + + " " + + " Normal" + + " false" + + " swift.finplus!pf" + + " SWIFTNet" + + " 005000" + + " 000000043" + + " " + + " pacs.009.001.08" + + " swift.cbprplus.02" + + " swi99998-2024-11-01T21:11:43.0016.645274Z" + + " SNL99998-2024-11-01T21:11:43.0016.575278Z" + + " 46ce8123-2844-4e86-afd0-438af82edc9e" + + " mtusus33_finplusfut!p" + + " 0301:2024-11-01T21:11:44" + + " 2024-11-01T21:11:44Z" + + " " + + " Success" + + " " + + " " + + " " + + " " + + " " + + " Success" + + " " + + " " + + " " + + " " + + " +W40vLwrVvZFJQzh0CiojwvLz074pkMqWr7oV5VlTaE=" + + " " + + " " + + " PEMF@Proc-Type: 4,MIC-ONLY" + + "Content-Domain: RFC822" + + "EntrustFile-Version: 2.0" + + "Originator-DN: cn=%51,cn=test,ou=transaction-signing,ou=transactionmanager,o=swift,o=swift" + + "Orig-SN: 1690692917" + + "MIC-Info: SHA256, RSA," + + " Ps/3auXoST3Y1S2EJ5swNkMS3gyfcVXH8rHnbo7uvimFX1NHd1R+AHsyejuih6Tx" + + " tRMX99SXFIkQu6VjRd7+r7NZ2zEoBVmb5T+pd3/OmrWd+LvtN6uvIyuPq3hTgz0t" + + " 9bbVrX3y1M98DkIiskXvD88haiUkSVonWuMIk++bwrithF9EXF/f5O5L+3NFoTte" + + " zZk+0qDdTxaVcj5TRTg9TH1a6UyHg58FpiWaBtlHPdkLIK5d4JCtU8oy92/sjFuz" + + " VJVs3ytyVI5oj/xa/VhFE4mYZELhLzrGK2iHdyP9fl4PIEO3TE+l06661/uQVg1s" + + " ndL8QMegjsAVCImtGQokag==" + + "" + + " " + + " cn=%51,cn=test,ou=transaction-signing,ou=transactionmanager,o=swift,o=swift" + + " 1.3.21.6.3.10.200.4" + + " " + + " " + + " " + + " TRD" + + " DWH/7ilKAtPS9bspbpgbdJQF8OrXzDFnp/eD3XBDiVQ=" + + " " + + " " + + " Sw.IARequestHeader" + + " vWsVhqcTYK2CsPw4rTKHHjuOvQuqRYQ71ER7HqRiX64=" + + " " + + " " + + " Sw.RequestPayload and RND" + + " C8tpELidZPb1ccAn1ndPytmX1qKwg1V+JQhtjlfIESY=" + + " " + + " " + + " " + + " UyolRk1TZFlATm8nTjYoK0g2cls=" + + " " + + " " + + " " + + " " + + " " + + " BnGorhNY5vlKv3bwChyHxJiEXYRtsMszPsYXadiIsF4=" + + " " + + " " + + " PEMF@Proc-Type: 4,MIC-ONLY" + + "Content-Domain: RFC822" + + "EntrustFile-Version: 2.0" + + "Originator-DN: cn=%6,cn=sfm,o=swift,o=swift" + + "Orig-SN: 1710047991" + + "MIC-Info: SHA256, RSA," + + " rlY3rtAnjwukxQaGQEtwPOAtdAbndJFinqCZ/XUyG+yF5MDj8fpl53kCoGrAwgTC" + + " CKzzc8+VWyf3tJCUsT1LwHoaY6wI7wyzdauY1sbxevSPqdPR+LdZqa+pn+w0o1GE" + + " 2tXjFzO0uVN/RJWKrwDUTdyk2hft69qb2UyXSYKDr1DLtOqPucP90SipbY/p/cK0" + + " 3pM0i687yizRlj/AyioIafCYpMkmwv9pd4y31AHHAzBidosAl8DOH23EFDr6nr+V" + + " SveapXDyM0HbF7yO9TPKaegHYN29QKbEeEwuGYcl3Jkf09nly63c14nv+Ennx+yJ" + + " ekbqgp3j1s7KjxpxKn1wzg==" + + "" + + " " + + " cn=%6,cn=sfm,o=swift,o=swift" + + " 1.3.21.6.3.10.100.3" + + " " + + " " + + " " + + " TRD" + + " DWH/7ilKAtPS9bspbpgbdJQF8OrXzDFnp/eD3XBDiVQ=" + + " " + + " " + + " Sw.IARequestHeader" + + " vWsVhqcTYK2CsPw4rTKHHjuOvQuqRYQ71ER7HqRiX64=" + + " " + + " " + + " Sw.RequestPayload and RND" + + " C8tpELidZPb1ccAn1ndPytmX1qKwg1V+JQhtjlfIESY=" + + " " + + " " + + " Translated MT" + + " FwpUmfkFHrRM4uilgM07fa8NsFmWe/0gYd5eKE3OTlQ=" + + " " + + " " + + " " + + " UyolRk1TZFlATm8nTjYoK0g2cls=" + + " " + + " " + + " " + + " " + + " " + + " 20241102204844" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " YYYXXX30XXX" + + " " + + " " + + " " + + " " + + " " + + " " + + " MTUZZZ30XXX" + + " " + + " " + + " " + + " MID/5506.0/a9689" + + " pacs.009.001.08" + + " swift.cbprplus.02" + + " 2024-11-01T22:11:15.532+01:00" + + " " + + " " + + " " + + " " + + " MID/5506.0/a9689" + + " 2024-11-01T22:11:15.532+01:00" + + " 1" + + " " + + " INGA" + + " " + + " " + + " " + + " " + + " IID/5506.0/a9689" + + " E2E/5506.0/7f159" + + " 2f093ecf-6129-486e-befd-1f9840c0c740" + + " " + + " " + + " " + + " G004" + + " " + + " " + + " 1000.0" + + " 2024-11-01" + + " " + + " " + + " YYYXXX30XXX" + + " " + + " " + + " " + + " " + + " MTUZZZ30XXX" + + " " + + " " + + " " + + " " + + " YYYABEB0XXX" + + " Default Bank" + + " " + + " Sample address" + + " " + + " " + + " " + + " " + + " " + + " YYYXXX30XXX" + + " " + + " " + + " " + + " " + + " MTUZZZ30XXX" + + " " + + " " + + " " + + " " + + " YYYCBEB0XXX" + + " Ganymede Bank" + + " " + + " Avenue des Consons, 214 b40" + + " 1214 Brussels" + + " Belgium" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + ""; + @Test public void testIdentifyMessage_01() { final String xml = "" @@ -90,40 +333,40 @@ public void testXxeDisabledInDetectMessage() { @Test public void testIdentifyMessage_FromBAH() { final String xml = "" - + "\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " FOOCUS3NXXX\n" - + " \n" - + " \n" - + " T2S\n" - + " \n" - + " ADMNUSERLUXCSDT1\n" - + " \n" - + " \n" - + " FOOTXE2SXXX\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " ABICUS33\n" - + " \n" - + " AARBDE5W100\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "2012111915360885\n" - + "seev.031.002.03 \n" - + "CSD \n" - + "2015-08-27T08:59:00Z\n" - + "\n" - + "\n" + + "" + + "" + + " " + + " " + + " " + + " FOOCUS3NXXX" + + " " + + " " + + " T2S" + + " " + + " ADMNUSERLUXCSDT1" + + " " + + " " + + " FOOTXE2SXXX" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " ABICUS33" + + " " + + " AARBDE5W100" + + " " + + " " + + " " + + " " + + "2012111915360885" + + "seev.031.002.03 " + + "CSD " + + "2015-08-27T08:59:00Z" + + "" + + "" + ""; MxId id = MxParseUtils.identifyMessage(xml).orElse(null); assertNotNull(id); @@ -134,7 +377,7 @@ public void testIdentifyMessage_FromBAH() { @Test public void testIdentifyMessage_LegacyBAH() { final String xml = "" - + "\n" + + "" + "" + " " + " " + " " @@ -153,10 +396,53 @@ public void testIdentifyMessage_LegacyBAH() { + " seev.037.002.02" + " 2017-08-08T16:58:01Z" + "" - + "\n" + + "" + ""; MxId id = MxParseUtils.identifyMessage(xml).orElse(null); assertNotNull(id); assertEquals("seev.037.002.02", id.id()); } + + @Test + public void testParseComments() { + List strings = MxParseUtils.parseComments(xml); + assertEquals(3, strings.size()); + assertEquals( + "{1:F01MTUZZZ30XXXX0000000000}{2:O2021711241101YYYXXX30XXXX00000000002411011711N}{3:{111:004}{121:2f093ecf-6359-486e-befd-1f9840c0c740}}{4:^~:20:IID/7706.0/a9689^~:21:E2E/7706.0/7f159^~:32A:241101EUR1000,^~:52A:YYYABEB0XXX^~:57A:MTUZZZ30XXX^~:58A:YYYCBEB0XXX^~:72:/INS/YYYXXX30XXX^~-}{5:{CHK:A87C6AB16C39}{TNG:}}", + strings.get(0)); + assertEquals("TranslationResult=TROK", strings.get(1)); + assertEquals("TranslationInfo version 1.0.0.1", strings.get(2)); + } + + @Test + public void testParseCommentsStartsWith() { + List strings = MxParseUtils.parseCommentsStartsWith(xml, "{1:F0"); + assertEquals(1, strings.size()); + assertEquals( + "{1:F01MTUZZZ30XXXX0000000000}{2:O2021711241101YYYXXX30XXXX00000000002411011711N}{3:{111:004}{121:2f093ecf-6359-486e-befd-1f9840c0c740}}{4:^~:20:IID/7706.0/a9689^~:21:E2E/7706.0/7f159^~:32A:241101EUR1000,^~:52A:YYYABEB0XXX^~:57A:MTUZZZ30XXX^~:58A:YYYCBEB0XXX^~:72:/INS/YYYXXX30XXX^~-}{5:{CHK:A87C6AB16C39}{TNG:}}", + strings.get(0)); + } + + @Test + public void testParseCommentsContains() { + List strings = MxParseUtils.parseCommentsContains(xml, "TROK"); + assertEquals(1, strings.size()); + assertEquals("TranslationResult=TROK", strings.get(0)); + } + + @Test + public void testParseMtFromMultiFormatMessage() { + Optional abstractMT = MxParseUtils.parseMtFromMultiformatMessage(xml); + assertTrue(abstractMT.isPresent()); + assertEquals("MTUZZZ30XXXX", abstractMT.get().getReceiver()); + assertEquals("YYYXXX30XXXX", abstractMT.get().getSender()); + assertEquals( + "/INS/YYYXXX30XXX", + abstractMT + .get() + .getSwiftMessage() + .getBlock4() + .getFieldByName("72") + .getValue()); + } }