From 5f62b9f6261532d99fc9073c0d3d76b8f4bca1ed Mon Sep 17 00:00:00 2001 From: zubri Date: Fri, 4 Jun 2021 19:30:42 -0300 Subject: [PATCH] MxWriteConfiguration and EscapeHandler API to tweak the serialization --- CHANGELOG.txt | 1 + build.gradle | 2 +- .../swift/model/mx/AbstractMX.java | 148 ++++++++++++------ .../swift/model/mx/AppHdr.java | 17 +- .../swift/model/mx/BusinessAppHdrV01.java | 10 +- .../swift/model/mx/BusinessAppHdrV02.java | 8 +- .../swift/model/mx/DefaultEscapeHandler.java | 61 ++++++++ .../swift/model/mx/EscapeHandler.java | 30 ++++ .../swift/model/mx/LegacyAppHdr.java | 10 +- .../swift/model/mx/MinimumEscapeHandler.java | 55 +++++++ .../swift/model/mx/MxWrite.java | 17 +- .../swift/model/mx/MxWriteConfiguration.java | 58 +++++++ .../swift/model/mx/MxWriteImpl.java | 35 ++++- .../swift/model/mx/XmlEventWriter.java | 56 +++---- .../com/prowidesoftware/issues/Issue24.java | 91 +++++++++++ 15 files changed, 497 insertions(+), 102 deletions(-) create mode 100644 iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/DefaultEscapeHandler.java create mode 100644 iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/EscapeHandler.java create mode 100644 iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MinimumEscapeHandler.java create mode 100644 iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteConfiguration.java create mode 100644 iso20022-core/src/test/java/com/prowidesoftware/issues/Issue24.java diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8820bccd6..91b06d199 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -4,6 +4,7 @@ RELEASE 9.1.7 - June 2021 * (GH-26) Fixed AppHdr JSON conversion with explicit new namespace field as discriminator + * (GH-24) Added a new MxWriteConfiguration and EscapeHandler API to tweak the serialization into XML RELEASE 9.1.6 - April 2021 * (GH-17|JIRA-506) Enhanced the XML format in the serializing, spaces and line breaks diff --git a/build.gradle b/build.gradle index bfd4ffa2d..ac5f12e5f 100644 --- a/build.gradle +++ b/build.gradle @@ -224,7 +224,7 @@ task writePom { } dependencies { - // included build (keep in sync with the latest Prowide Core version) + // included build (keep in sync with the latest Prowide Core version, this goes to Maven transitive dependency) api 'com.prowidesoftware:pw-swift-core:SRU2020-9.1.4' implementation 'org.apache.commons:commons-lang3:3.8.1' implementation 'com.google.code.gson:gson:2.8.2' diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AbstractMX.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AbstractMX.java index 2c4bf33ca..7d654ea4a 100644 --- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AbstractMX.java +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AbstractMX.java @@ -43,6 +43,7 @@ import javax.xml.transform.dom.DOMResult; import javax.xml.transform.stream.StreamSource; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; @@ -66,14 +67,17 @@ * @since 7.6 */ public abstract class AbstractMX extends AbstractMessage implements IDocument, JsonSerializable { - public static final String DOCUMENT_LOCALNAME = "Document"; private static final transient Logger log = Logger.getLogger(AbstractMX.class.getName()); + + public static final String DOCUMENT_LOCALNAME = "Document"; + /** * Default root element when an MX is serialized as XML including both AppHdr and Document * * @since 8.0.2 */ public static String DEFAULT_ROOT_ELEMENT = "RequestPayload"; + /** * Header portion of the message payload, common to all specific MX subclasses. * This information is required before opening the actual message to process the content properly. @@ -93,8 +97,20 @@ protected AbstractMX(final AppHdr appHdr) { this.appHdr = appHdr; } + /** + * @deprecated use {@link #message(String, AbstractMX, Class[], String, boolean, EscapeHandler)} instead + */ + @Deprecated + @ProwideDeprecated(phase2 = TargetYear.SRU2022) protected static String message(final String namespace, final AbstractMX obj, @SuppressWarnings("rawtypes") final Class[] classes, final String prefix, boolean includeXMLDeclaration) { - return MxWriteImpl.write(namespace, obj, classes, prefix, includeXMLDeclaration); + return message(namespace, obj, classes, prefix, includeXMLDeclaration, null); + } + + /** + * @since 9.1.7 + */ + protected static String message(final String namespace, final AbstractMX obj, @SuppressWarnings("rawtypes") final Class[] classes, final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) { + return MxWriteImpl.write(namespace, obj, classes, prefix, includeXMLDeclaration, escapeHandler); } @SuppressWarnings({"rawtypes", "unchecked"}) @@ -193,7 +209,7 @@ protected static T fromJson(String json, Class classOfT) { final Gson gson = new GsonBuilder() .registerTypeAdapter(AbstractMX.class, new AbstractMXAdapter()) .registerTypeAdapter(XMLGregorianCalendar.class, new XMLGregorianCalendarAdapter()) - .registerTypeAdapter(AppHdr.class, new AppHdrAdapter()) + .registerTypeAdapter(AppHdr.class, new AppHdrAdapter()) .create(); return gson.fromJson(json, classOfT); } @@ -209,7 +225,7 @@ public static AbstractMX fromJson(String json) { final Gson gson = new GsonBuilder() .registerTypeAdapter(AbstractMX.class, new AbstractMXAdapter()) .registerTypeAdapter(XMLGregorianCalendar.class, new XMLGregorianCalendarAdapter()) - .registerTypeAdapter(AppHdr.class, new AppHdrAdapter()) + .registerTypeAdapter(AppHdr.class, new AppHdrAdapter()) .create(); return gson.fromJson(json, AbstractMX.class); } @@ -263,76 +279,99 @@ public static AbstractMX fromJson(String json) { /** * Get this message as an XML string. + * *

If the header is present, then 'AppHdr' and 'Document' elements will be wrapped under a - * {@link #DEFAULT_ROOT_ELEMENT} - *
Both header and documents are generated with the corresponding namespaces and by default the prefix 'h' is - * used for the header and the prefix 'Doc' for the document. + * {@link #DEFAULT_ROOT_ELEMENT}. Both header and document are generated with the corresponding namespaces and by + * default the prefix 'h' is used for the header and the prefix 'Doc' for the document. + *
For more serialization options see {@link #message(MxWriteConfiguration)} + *
To serialize only the header or the document (without header) see {@link #header()} and {@link #document()} * - * @see #message(String, boolean) + * @return the XML content or null if errors occur during serialization * @since 7.7 */ @Override public String message() { - return message(null, true); + return message((MxWriteConfiguration) null); + } + + /** + * @deprecated use {@link #message(MxWriteConfiguration)} instead + */ + @Deprecated + @ProwideDeprecated(phase2 = TargetYear.SRU2022) + public String message(final String rootElement, boolean includeXMLDeclaration) { + MxWriteConfiguration conf = new MxWriteConfiguration(); + conf.rootElement = rootElement; + conf.includeXMLDeclaration = includeXMLDeclaration; + return message(conf); + } + + /** + * @deprecated use {@link #message(MxWriteConfiguration)} instead + */ + @Deprecated + @ProwideDeprecated(phase2 = TargetYear.SRU2022) + public String message(final String rootElement) { + MxWriteConfiguration conf = new MxWriteConfiguration(); + conf.rootElement = rootElement; + return message(conf); } /** * Get this message as an XML string. * *

If the business header is set, the created XML will include both the 'AppHdr' and the 'Document' elements, - * under a the indicated or default root element. - *
If the header is not present, the created XMl will only include the 'Document'. - *
Both 'AppHdr' and 'Document' are generated with namespace declaration and default prefixes 'h' and 'Doc' - * respectively. + * under a the indicated or default root element. If the header is not present, the created XMl will only include + * the 'Document'. Both 'AppHdr' and 'Document' are generated with namespace declaration and if optional prefixes + * if present in the configuration. * *

IMPORTANT: The name of the envelope element that binds a Header to the message to which it applies is * implementation/network specific. The header root element ‘AppHdr’ and the ISO 20022 MessageDefinition * root element ‘Document’ must always be sibling elements in any XML document, with the AppHdr element preceding * the Document element. * - * @param rootElement optional specification of the root element if not provided {@link #DEFAULT_ROOT_ELEMENT} is used - * @param includeXMLDeclaration true to include the XML declaration - * @return header serialized into XML string or null if the header is not set or errors occur during serialization - * @return created XML - * @since 7.8 + * @param conf specific options for the serialization or null to use the default parameters + * @return the XML content or null if errors occur during serialization */ - public String message(final String rootElement, boolean includeXMLDeclaration) { - String root = rootElement != null ? rootElement : DEFAULT_ROOT_ELEMENT; + public String message(MxWriteConfiguration conf) { + MxWriteConfiguration usableConf = conf != null? conf : new MxWriteConfiguration(); + String root = usableConf.rootElement; StringBuilder xml = new StringBuilder(); - if (includeXMLDeclaration) { + if (usableConf.includeXMLDeclaration) { xml.append("\n"); } - final String header = header("h", false); + final String header = header(usableConf.headerPrefix, false, usableConf.escapeHandler); if (header != null) { - xml.append("<" + root + ">\n"); - xml.append(header + "\n"); + xml.append("<").append(root).append(">\n"); + xml.append(header).append("\n"); } - xml.append(document("Doc", false) + "\n"); + xml.append(document(usableConf.documentPrefix, false, usableConf.escapeHandler)).append("\n"); if (header != null) { - xml.append(""); + xml.append(""); } return xml.toString(); } /** - * Same as {@link #message(String, boolean)} with includeXMLDeclaration set to true + * Get this message AppHdr as an XML string. * + *

The XML will not include the XML declaration, will bind the namespace to all elements without prefix and will + * use the default escape handler. For more serialization options use {@link #header(String, boolean, EscapeHandler)} + * + * @return the serialized header or null if header is not set or errors occur during serialization * @since 7.8 */ - public String message(final String rootElement) { - return message(rootElement, true); + public String header() { + return header(null, false, null); } /** - * Get this message AppHdr as an XML string. - *

The XML will not include the XML declaration, and will include de namespace as default (without prefix). - * - * @return the serialized header or null if header is not set or errors occur during serialization - * @see #header(String, boolean) - * @since 7.8 + * @deprecated use {@link #header(String, boolean, EscapeHandler)} instead */ - public String header() { - return header(null, false); + @Deprecated + @ProwideDeprecated(phase2 = TargetYear.SRU2022) + public String header(final String prefix, boolean includeXMLDeclaration) { + return header(prefix, includeXMLDeclaration, null); } /** @@ -340,12 +379,13 @@ public String header() { * * @param prefix optional prefix for namespace (empty by default) * @param includeXMLDeclaration true to include the XML declaration + * @param escapeHandler a specific escape handler for the header elements content * @return header serialized into XML string or null if the header is not set or errors occur during serialization - * @since 7.8 + * @since 9.1.7 */ - public String header(final String prefix, boolean includeXMLDeclaration) { + public String header(final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) { if (this.appHdr != null) { - return this.appHdr.xml(prefix, includeXMLDeclaration); + return this.appHdr.xml(prefix, includeXMLDeclaration, escapeHandler); } else { return null; } @@ -353,26 +393,38 @@ public String header(final String prefix, boolean includeXMLDeclaration) { /** * Get this message Document as an XML string. - *

The XML will include the XML declaration, and will use "Doc" as prefix for the elements. + * + *

The XML will not include the XML declaration, will bind the namespace to all elements using "Doc" as default + * prefix and will use the default escape handler. For more serialization options use + * {@link #document(String, boolean, EscapeHandler)} * * @return document serialized into XML string or null if errors occur during serialization - * @see #document(String, boolean) * @since 7.8 */ public String document() { return document("Doc", true); } + /** + * @deprecated use {@link #document(String, boolean, EscapeHandler)} instead + */ + @Deprecated + @ProwideDeprecated(phase2 = TargetYear.SRU2022) + public String document(final String prefix, boolean includeXMLDeclaration) { + return document(prefix, includeXMLDeclaration, null); + } + /** * Get this message Document as an XML string. * * @param prefix optional prefix for namespace (empty by default) * @param includeXMLDeclaration true to include the XML declaration + * @param escapeHandler a specific escape handler for the document elements content * @return document serialized into XML string or null if errors occur during serialization - * @since 7.8 + * @since 9.1.7 */ - public String document(final String prefix, boolean includeXMLDeclaration) { - return message(getNamespace(), this, getClasses(), prefix, includeXMLDeclaration); + public String document(final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) { + return message(getNamespace(), this, getClasses(), prefix, includeXMLDeclaration, escapeHandler); } /** @@ -392,7 +444,7 @@ public Source xmlSource() { } /** - * Writes the message document content into a file in XML format (headers not included). + * Writes the message content into a file in XML format. * * @param file a not null file to write, if it does not exists, it will be created * @since 7.7 @@ -417,7 +469,7 @@ public void write(final File file) throws IOException { */ public void write(final OutputStream stream) throws IOException { Validate.notNull(stream, "the stream to write cannot be null"); - stream.write(message().getBytes("UTF-8")); + stream.write(message().getBytes(StandardCharsets.UTF_8)); } /** @@ -521,7 +573,7 @@ public String toJson() { final Gson gson = new GsonBuilder() .registerTypeAdapter(AbstractMX.class, new AbstractMXAdapter()) .registerTypeAdapter(XMLGregorianCalendar.class, new XMLGregorianCalendarAdapter()) - .registerTypeAdapter(AppHdr.class, new AppHdrAdapter()) + .registerTypeAdapter(AppHdr.class, new AppHdrAdapter()) .setPrettyPrinting() .create(); // we use AbstractMX and not this.getClass() in order to force usage of the adapter diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AppHdr.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AppHdr.java index 3fba81e41..73de87a83 100644 --- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AppHdr.java +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AppHdr.java @@ -114,7 +114,7 @@ public interface AppHdr { * Get this header as an XML string. *

The implementation uses {@link #xml(String, boolean)} with no prefix and no XML declaration. * - * @return header serialized into XML string or null if neither header version is present + * @return header serialized into XML string or null in case of unexpected error */ default String xml() { return xml(null, false); @@ -125,9 +125,23 @@ default String xml() { * * @param prefix optional prefix for namespace (empty by default) * @param includeXMLDeclaration true to include the XML declaration (false by default) + * @return header serialized into XML string or null in case of unexpected error */ String xml(final String prefix, boolean includeXMLDeclaration); + /** + * Get this header as an XML string. + * + * @param prefix optional prefix for namespace (empty by default) + * @param includeXMLDeclaration true to include the XML declaration (false by default) + * @param escapeHandler a specific escape handler for the header elements content + * @return header serialized into XML string or null in case of unexpected error + * @since 9.1.7 + */ + default String xml(final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) { + return xml(prefix, includeXMLDeclaration); + } + /** * Gets the header as an Element object. */ @@ -135,6 +149,7 @@ default String xml() { /** * Gets the specific namespace of the header + * * @return default implementation returns null * @since 9.1.7 */ diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV01.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV01.java index 96171d432..e20b1fe39 100644 --- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV01.java +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV01.java @@ -185,14 +185,20 @@ public void setCreationDate(boolean overwrite) { } @Override - public String xml(String prefix, boolean includeXMLDeclaration) { + public String xml(final String prefix, boolean includeXMLDeclaration) { + return xml(prefix, includeXMLDeclaration, null); + } + + @Override + public String xml(String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) { try { JAXBContext context = JAXBContext.newInstance(BusinessApplicationHeaderV01Impl.class); final Marshaller marshaller = context.createMarshaller(); final StringWriter sw = new StringWriter(); JAXBElement element = new JAXBElement(new QName(NAMESPACE, AppHdr.HEADER_LOCALNAME), BusinessApplicationHeaderV01Impl.class, null, this); - marshaller.marshal(element, new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME)); + XmlEventWriter eventWriter = new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME, escapeHandler); + marshaller.marshal(element, eventWriter); return sw.getBuffer().toString(); } catch (JAXBException e) { diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV02.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV02.java index 8b5ccbd24..ca77a39ca 100644 --- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV02.java +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV02.java @@ -186,13 +186,19 @@ public void setCreationDate(boolean overwrite) { @Override public String xml(String prefix, boolean includeXMLDeclaration) { + return xml(prefix, includeXMLDeclaration, null); + } + + @Override + public String xml(String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) { try { JAXBContext context = JAXBContext.newInstance(BusinessApplicationHeaderV02Impl.class); final Marshaller marshaller = context.createMarshaller(); final StringWriter sw = new StringWriter(); JAXBElement element = new JAXBElement(new QName(NAMESPACE, AppHdr.HEADER_LOCALNAME), BusinessApplicationHeaderV02Impl.class, null, this); - marshaller.marshal(element, new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME)); + XmlEventWriter eventWriter = new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME, escapeHandler); + marshaller.marshal(element, eventWriter); return sw.getBuffer().toString(); } catch (JAXBException e) { diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/DefaultEscapeHandler.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/DefaultEscapeHandler.java new file mode 100644 index 000000000..5221b5037 --- /dev/null +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/DefaultEscapeHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2006-2021 Prowide + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.prowidesoftware.swift.model.mx; + +/** + * Escapes &, <, >, quotes (in attribute) and everything above the US-ASCII code range. + * Similar to com.sun.xml.bind.marshaller.DumbEscapeHandler or apache.commons.text.StringEscapeUtils#escapeXml + * + * @since 9.1.7 + */ +public class DefaultEscapeHandler implements EscapeHandler { + + @Override + public String escape(char[] arr, boolean isAttribute) { + final StringBuilder sb = new StringBuilder(arr.length); + for (int i = 0; i < arr.length; i++) { + switch (arr[i]) { + case '&': + sb.append("&"); + break; + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '\"': + if (isAttribute) { + sb.append("""); + } else { + sb.append('\"'); + } + sb.append('\"'); + break; + default: + if (arr[i] > '\u007f') { + sb.append("&#"); + sb.append(Integer.toString(arr[i])); + sb.append(';'); + } else { + sb.append(arr[i]); + } + } + } + return sb.toString(); + } + +} diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/EscapeHandler.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/EscapeHandler.java new file mode 100644 index 000000000..d22e9aa38 --- /dev/null +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/EscapeHandler.java @@ -0,0 +1,30 @@ +/* + * Copyright 2006-2021 Prowide + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.prowidesoftware.swift.model.mx; + +/** + * Simple interface used by the XML event writer when serializing the XML element content + * @since 9.1.7 + */ +public interface EscapeHandler { + + /** + * @param arr the characters to escape + * @param isAttribute true if it is an attribute value + */ + String escape(char[] arr, boolean isAttribute); + +} diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/LegacyAppHdr.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/LegacyAppHdr.java index 9e6002735..5e478249b 100644 --- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/LegacyAppHdr.java +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/LegacyAppHdr.java @@ -175,14 +175,20 @@ public void setCreationDate(boolean overwrite) { } @Override - public String xml(String prefix, boolean includeXMLDeclaration) { + public String xml(final String prefix, boolean includeXMLDeclaration) { + return xml(prefix, includeXMLDeclaration, null); + } + + @Override + public String xml(String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) { try { JAXBContext context = JAXBContext.newInstance(ApplicationHeaderImpl.class); final Marshaller marshaller = context.createMarshaller(); final StringWriter sw = new StringWriter(); JAXBElement element = new JAXBElement(new QName(NAMESPACE, AppHdr.HEADER_LOCALNAME), ApplicationHeaderImpl.class, null, this); - marshaller.marshal(element, new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME)); + XmlEventWriter eventWriter = new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME, escapeHandler); + marshaller.marshal(element, eventWriter); return sw.getBuffer().toString(); } catch (JAXBException e) { diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MinimumEscapeHandler.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MinimumEscapeHandler.java new file mode 100644 index 000000000..cd1813dcb --- /dev/null +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MinimumEscapeHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright 2006-2021 Prowide + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.prowidesoftware.swift.model.mx; + +/** + * Only escapes &, <, > and quotes (in attribute). + * Similar to com.sun.xml.bind.marshaller.MinimumEscapeHandler + * + * @since 9.1.7 + */ +public class MinimumEscapeHandler implements EscapeHandler { + + @Override + public String escape(char[] arr, boolean isAttribute) { + final StringBuilder sb = new StringBuilder(arr.length); + for (int i = 0; i < arr.length; i++) { + switch (arr[i]) { + case '&': + sb.append("&"); + break; + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '\"': + if (isAttribute) { + sb.append("""); + } else { + sb.append('\"'); + } + sb.append('\"'); + break; + default: + sb.append(arr[i]); + } + } + return sb.toString(); + } + +} diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWrite.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWrite.java index 1d9d49d4c..54d152e63 100644 --- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWrite.java +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWrite.java @@ -15,6 +15,9 @@ */ package com.prowidesoftware.swift.model.mx; +import com.prowidesoftware.deprecation.ProwideDeprecated; +import com.prowidesoftware.deprecation.TargetYear; + /** * Interface to plug in code that serializes MX message objects to XML string * @@ -22,6 +25,13 @@ */ public interface MxWrite { + /** + * @deprecated use {@link #message(String, AbstractMX, Class[], String, boolean, EscapeHandler)} instead + */ + @Deprecated + @ProwideDeprecated(phase2 = TargetYear.SRU2022) + String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration); + /** * Converts obj into a xml string * @@ -30,9 +40,12 @@ public interface MxWrite { * @param classes array of all classes used or referenced by message class * @param prefix optional prefix for ns ("Doc" by default) * @param includeXMLDeclaration true to include the xml declaration (true by default) + * @param escapeHandler specific escape handler to use when serializing the elements content or null to use the default * @return the message content serialized to XML - * @since 7.8 + * @since 9.1.7 */ - String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration); + default String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) { + return message(namespace, obj, classes, prefix, includeXMLDeclaration, null); + } } diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteConfiguration.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteConfiguration.java new file mode 100644 index 000000000..b663eb10d --- /dev/null +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2006-2021 Prowide + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.prowidesoftware.swift.model.mx; + +/** + * Options POJO to customize the behaviour of the MX writer (model into XML serialization) + * + * @since 9.1.7 + */ +public class MxWriteConfiguration { + + /** + * The name of the envelope element that binds a Header to the message to which it applies is + * implementation/network specific. The header root element ‘AppHdr’ and the ISO 20022 MessageDefinition + * root element ‘Document’ must always be sibling elements in any XML document, with the AppHdr element preceding + * the Document element. If no root elemewnt name is provided the value in {@link AbstractMX#DEFAULT_ROOT_ELEMENT} + * is used as default + */ + public String rootElement = AbstractMX.DEFAULT_ROOT_ELEMENT; + + /** + * Determines if the XML will include the XML declaration as first line. It is true by default. You can switch this + * off if the generated XML will then be used a a fragment of another XML wrapper. + */ + public boolean includeXMLDeclaration = true; + + /** + * Enables switching between different implementations for the element and attributes value escaping. Some + * implementations are available in the library and your own custom class can also be used. This is useful if you + * handle XML messages with specific charset and you want to control what is escaped and what is propagated as is. + */ + public EscapeHandler escapeHandler = new DefaultEscapeHandler(); + + /** + * The prefix for the header namespace. Set it to null if you don't want to have any prefix in header elements. + * It is "h" by default. + */ + public String headerPrefix = "h"; + + /** + * The prefix for the document namespace. Set it to null if you don't want to have any prefix in document elements. + * It is "Doc" by default. + */ + public String documentPrefix = "Doc"; +} diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteImpl.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteImpl.java index 757ae17a7..d1e5964f6 100644 --- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteImpl.java +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteImpl.java @@ -15,6 +15,8 @@ */ package com.prowidesoftware.swift.model.mx; +import com.prowidesoftware.deprecation.ProwideDeprecated; +import com.prowidesoftware.deprecation.TargetYear; import com.prowidesoftware.swift.model.MxSwiftMessage; import org.apache.commons.lang3.Validate; @@ -40,11 +42,20 @@ public class MxWriteImpl implements MxWrite { private static final transient Logger log = Logger.getLogger(MxWriteImpl.class.getName()); /** - * Static serialization implementation of {@link MxWrite#message(String, AbstractMX, Class[], String, boolean)} - * - * @since 9.0 + * @deprecated use {@link #message(String, AbstractMX, Class[], String, boolean, EscapeHandler)} instead */ + @Deprecated + @ProwideDeprecated(phase2 = TargetYear.SRU2022) public static String write(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration) { + return write(namespace, obj, classes, prefix, includeXMLDeclaration, null); + } + + /** + * Static serialization implementation of {@link MxWrite#message(String, AbstractMX, Class[], String, boolean, EscapeHandler)} + * + * @since 9.1.7 + */ + public static String write(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) { Validate.notNull(namespace, "namespace can not be null"); Validate.notNull(obj, "MxSwiftMessage can not be null"); Validate.notNull(classes, "Class[] can not be null"); @@ -58,7 +69,7 @@ public static String write(String namespace, AbstractMX obj, Class[] classes, fi final Marshaller marshaller = context.createMarshaller(); final StringWriter sw = new StringWriter(); - XmlEventWriter writer = new XmlEventWriter(sw, prefix, includeXMLDeclaration, "Document"); + XmlEventWriter writer = new XmlEventWriter(sw, prefix, includeXMLDeclaration, "Document", escapeHandler); Map preferredPrefixes = new HashMap<>(); for (XsysNamespaces xsys : XsysNamespaces.values()) { @@ -79,14 +90,24 @@ public static String write(String namespace, AbstractMX obj, Class[] classes, fi return null; } + /** + * @deprecated use {@link #message(String, AbstractMX, Class[], String, boolean, EscapeHandler)} instead + */ + @Deprecated + @ProwideDeprecated(phase2 = TargetYear.SRU2022) + @Override + public String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration) { + return write(namespace, obj, classes, prefix, includeXMLDeclaration, null); + } + /** * Implements serialization to XML * - * @see MxWrite#message(String, AbstractMX, Class[], String, boolean) + * @see MxWrite#message(String, AbstractMX, Class[], String, boolean, EscapeHandler) */ @Override - public String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration) { - return write(namespace, obj, classes, prefix, includeXMLDeclaration); + public String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) { + return write(namespace, obj, classes, prefix, includeXMLDeclaration, escapeHandler); } } diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/XmlEventWriter.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/XmlEventWriter.java index a852f7f05..b9a550a62 100644 --- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/XmlEventWriter.java +++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/XmlEventWriter.java @@ -53,21 +53,34 @@ public final class XmlEventWriter implements XMLEventWriter { private boolean preserveQnamePrefixes = false; private int previousNestedStartLevel; private XMLEvent previousEvent; + private EscapeHandler escapeHandler; + + /** + * @deprecated use {@link #XmlEventWriter(Writer, String, boolean, String, EscapeHandler)} instead + */ + @Deprecated + @ProwideDeprecated(phase2 = TargetYear.SRU2022) + public XmlEventWriter(Writer baos, final String defaultPrefix, boolean includeXMLDeclaration, final String rootElement) { + this(baos, defaultPrefix, includeXMLDeclaration, rootElement, null); + } /** * @param baos output buffer to write * @param defaultPrefix optional prefix (empty by default) to used for all elements that are not binded to a specific prefix * @param includeXMLDeclaration true to include the XML declaration (true by default) * @param rootElement local name of the root element of the XML fragment to create, used to declare namespace + * @param escapeHandler escape handler to use or null to use the default * @see #setPreferredPrefixes(Map) + * @since 9.1.7 */ - public XmlEventWriter(Writer baos, final String defaultPrefix, boolean includeXMLDeclaration, final String rootElement) { + public XmlEventWriter(Writer baos, final String defaultPrefix, boolean includeXMLDeclaration, final String rootElement, final EscapeHandler escapeHandler) { this.out = baos; this.startElementCount = 0; this.nestedLevel = 0; this.defaultPrefix = defaultPrefix; this.includeXMLDeclaration = includeXMLDeclaration; this.rootElement = rootElement; + this.escapeHandler = escapeHandler != null ? escapeHandler : new DefaultEscapeHandler(); } public void add(final XMLEvent event) throws XMLStreamException { @@ -141,7 +154,8 @@ public void add(final XMLEvent event) throws XMLStreamException { break; } final char[] arr = ce.getData().toCharArray(); - out.write(escape(arr)); + String escapedString = this.escapeHandler.escape(arr, false); + out.write(escapedString); this.previousEvent = event; break; } @@ -187,7 +201,8 @@ public void add(final XMLEvent event) throws XMLStreamException { case XMLEvent.ATTRIBUTE: { final Attribute a = (Attribute) event; - out.write(" " + a.getName() + "=\"" + a.getValue() + "\""); + String escapedString = a.getValue() != null ? this.escapeHandler.escape(a.getValue().toCharArray(), true) : ""; + out.write(" " + a.getName() + "=\"" + escapedString + "\""); this.previousEvent = event; break; } @@ -231,41 +246,6 @@ private String namespace(final Namespace namespace) { return sb.toString(); } - /** - * Inplace escape por xml - * - * @since 7.8 - */ - private String escape(char[] arr) { - final StringBuilder sb = new StringBuilder(arr.length); - // TODO Consider code in com.sun.xml.bind.marshaller.DumbEscapeHandler for replacements - for (int i = 0; i < arr.length; i++) { - switch (arr[i]) { - case '&': - sb.append("&"); - break; - case '<': - sb.append("<"); - break; - case '>': - sb.append(">"); - break; - case '\"': - sb.append('\"'); - break; - default: - if (arr[i] > '\u007f') { - sb.append("&#"); - sb.append(Integer.toString(arr[i])); - sb.append(';'); - } else { - sb.append(arr[i]); - } - } - } - return sb.toString(); - } - private String prefixString(final QName qname) { String prefix = resolvePrefix(qname); if (prefix != null) { diff --git a/iso20022-core/src/test/java/com/prowidesoftware/issues/Issue24.java b/iso20022-core/src/test/java/com/prowidesoftware/issues/Issue24.java new file mode 100644 index 000000000..2591cb541 --- /dev/null +++ b/iso20022-core/src/test/java/com/prowidesoftware/issues/Issue24.java @@ -0,0 +1,91 @@ +/* + * Copyright 2006-2021 Prowide + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.prowidesoftware.issues; + +import com.prowidesoftware.swift.model.mx.MinimumEscapeHandler; +import com.prowidesoftware.swift.model.mx.MxPain00100103; +import com.prowidesoftware.swift.model.mx.MxWriteConfiguration; +import com.prowidesoftware.swift.model.mx.dic.CustomerCreditTransferInitiationV03; +import com.prowidesoftware.swift.model.mx.dic.GroupHeader32; +import com.prowidesoftware.swift.model.mx.dic.PartyIdentification32; +import com.prowidesoftware.swift.model.mx.dic.PaymentInstructionInformation3; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * https://github.com/prowide/prowide-iso20022/issues/24 + */ +public class Issue24 { + + private MxPain00100103 sample() { + MxPain00100103 mx = new MxPain00100103(); + mx.setCstmrCdtTrfInitn(new CustomerCreditTransferInitiationV03()); + mx.getCstmrCdtTrfInitn().setGrpHdr(new GroupHeader32()); + mx.getCstmrCdtTrfInitn().getGrpHdr().setMsgId("1234"); + mx.getCstmrCdtTrfInitn().addPmtInf(new PaymentInstructionInformation3()); + mx.getCstmrCdtTrfInitn().getPmtInf().get(0).setDbtr(new PartyIdentification32()); + mx.getCstmrCdtTrfInitn().getPmtInf().get(0).getDbtr().setNm("текст текст öñ"); + return mx; + } + + @Test + public void testWriteDefaultEscapeHandler() { + MxPain00100103 mx = sample(); + String xml = mx.message(); + assertTrue(xml.contains("текст текст öñ")); + + MxPain00100103 mx2 = MxPain00100103.parse(xml); + assertEquals("текст текст öñ", mx2.getCstmrCdtTrfInitn().getPmtInf().get(0).getDbtr().getNm()); + assertEquals(mx, mx2); + } + + @Test + public void testWriteDefaultMinimumEscapeHandler() { + MxPain00100103 mx = sample(); + + MxWriteConfiguration conf = new MxWriteConfiguration(); + conf.escapeHandler = new MinimumEscapeHandler(); + String xml = mx.message(conf); + + assertTrue(xml.contains("текст текст öñ")); + + MxPain00100103 mx2 = MxPain00100103.parse(xml); + assertEquals("текст текст öñ", mx2.getCstmrCdtTrfInitn().getPmtInf().get(0).getDbtr().getNm()); + assertEquals(mx, mx2); + } + + @Test + public void testParse() { + String xml = "\n" + + "\n" + + " \n" + + " \n" + + " 1234\n" + + " \n" + + " \n" + + " \n" + + " текст текст öñ\n" + + " \n" + + " \n" + + " \n" + + ""; + MxPain00100103 mx2 = MxPain00100103.parse(xml); + assertEquals("текст текст öñ", mx2.getCstmrCdtTrfInitn().getPmtInf().get(0).getDbtr().getNm()); + } + +}