com.prowidesoftware.swift.model.mx.AbstractMX Maven / Gradle / Ivy
Show all versions of pw-iso20022 Show documentation
/*
* Copyright 2006-2023 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;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.prowidesoftware.JsonSerializable;
import com.prowidesoftware.deprecation.DeprecationUtils;
import com.prowidesoftware.deprecation.ProwideDeprecated;
import com.prowidesoftware.deprecation.TargetYear;
import com.prowidesoftware.swift.model.AbstractMessage;
import com.prowidesoftware.swift.model.MessageStandardType;
import com.prowidesoftware.swift.model.MxId;
import com.prowidesoftware.swift.model.mt.AbstractMT;
import com.prowidesoftware.swift.model.mx.adapters.*;
import com.prowidesoftware.swift.model.mx.adapters.v9.V9DateTimeJsonAdapter;
import com.prowidesoftware.swift.utils.Lib;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.annotation.XmlTransient;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;
/**
* Base class for specific MX messages.
*
* IMPORTANT: An MX message is conformed by a set of optional headers and a message payload or document with the actual
* specific MX message. The name of the envelope element that binds a Header to the message to which it applies is
* implementation/network specific and not part of the scope of this model.
*
*
This class provides the base container model for MX messages including an attribute for the header. Further it
* supports both versions for the header; the SWIFT Application Header (legacy) and the ISO Business Application Header.
*
*
Serialization of this model into XML text can be done for the with or without the header portion. When the header
* is set and included into the serialization, the container root element must be provided.
*
* @see AbstractMT
* @since 7.6
*/
public abstract class AbstractMX extends AbstractMessage implements JsonSerializable {
public static final String DOCUMENT_LOCALNAME = "Document";
private static final Logger log = Logger.getLogger(AbstractMX.class.getName());
/**
* @deprecated the default root element for the custom envelope is now defined in {@link EnvelopeType#CUSTOM}
*/
@Deprecated
@ProwideDeprecated(phase2 = TargetYear.SRU2025)
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.
*
* @since 7.7 original field using BusinessHeader class
* @since 9.0.1 changed to interface AppHdr
*/
private AppHdr appHdr;
protected AbstractMX() {
super(MessageStandardType.MX);
// prevent construction
}
protected AbstractMX(final AppHdr appHdr) {
super(MessageStandardType.MX);
this.appHdr = appHdr;
}
/**
* Parses the XML string containing the Document and optional AppHdr into a specific instance of MX message object.
* The message and header types and version is auto-detected.
*
*
The unmarshaller uses the default type adapters. For more parse options use {@link #parse(String, MxId, MxReadConfiguration)}.
*
* @param xml the XML content to parse
* @return parsed message or null if string content could not be parsed into an Mx
* @since 9.0.1
*/
public static AbstractMX parse(final String xml) {
return MxReadImpl.parse(xml, null, new MxReadParams());
}
/**
* Parses the XML string containing the Document and optional AppHdr into a specific instance of MX message object.
* The header version, if present, is auto-detected from its namespace.
*
*
If the string is empty, does not contain an MX document, the message type cannot be detected or an error
* occur reading and parsing the message content; this method returns null.
*
*
The implementation detects the message type and uses reflection to call the parser in the specific subclass.
*
*
The unmarshaller uses the default type adapters. For more parse options use {@link #parse(String, MxId, MxReadConfiguration)}.
*
* @param xml string a string containing the Document of an MX message in XML format
* @param id optional parameter to indicate the specific MX type to create; auto detected from namespace if null.
* @return parsed message or null if string content could not be parsed into an Mx
* @since 7.8.4
*/
public static AbstractMX parse(final String xml, MxId id) {
return MxReadImpl.parse(xml, id, new MxReadParams());
}
/**
* @deprecated use Lib.readFile(file) and any parse from String method
*/
@Deprecated
@ProwideDeprecated(phase4 = TargetYear.SRU2025)
public static AbstractMX parse(final File file, MxId id) throws IOException {
DeprecationUtils.phase3(
AbstractMX.class, "parse(File, MxId)", "Use Lib.readFile(file) and any parse from String method");
return MxReadImpl.parse(Lib.readFile(file), id, new MxReadParams());
}
/**
* Parses the XML Element into a specific MX object.
* The Mx to create is detected from the namespace.
*
* @param e content to parse
* @return specific Mx instance, for example, MxCamt0010101; or null if XML cannot be parsed or specific Mx cannot be detected
* @since 9.0
*/
public static AbstractMX parse(final Element e) {
final String prefix = e.getPrefix();
if (prefix != null) {
NamedNodeMap attributes = e.getAttributes();
List removables = new ArrayList<>();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
if (!StringUtils.equals(attr.getNodeName(), "xmlns:" + prefix)
&& !StringUtils.equals(attr.getNodeName(), "xmlns:xsi")) {
removables.add(attr);
}
}
for (Node attr : removables) {
attributes.removeNamedItem(attr.getNodeName());
}
}
DOMImplementationLS lsImpl =
(DOMImplementationLS) e.getOwnerDocument().getImplementation().getFeature("LS", "3.0");
LSSerializer serializer = lsImpl.createLSSerializer();
serializer.getDomConfig().setParameter("xml-declaration", false);
String xml = serializer.writeToString(e);
if (e.getNamespaceURI() != null) {
return AbstractMX.parse(xml, new MxId(e.getNamespaceURI()), new MxReadConfiguration());
}
return null;
}
/**
* Parses the XML string containing the Document and optional AppHdr into a specific instance of MX message object.
* The header version, if present, is auto detected from its namespace.
*
* If the string is empty, does not contain an MX document, the message type cannot be detected or an error
* occur reading and parsing the message content; this method returns null.
*
*
The implementation detects the message type and uses reflection to call the parser in the specific subclass.
*
* @param xml string a string containing the Document of an MX message in XML format
* @param id optional parameter to indicate the specific MX type to create; auto detected from namespace if null.
* @param conf specific options for the unmarshalling or null to use the default parameters
* @return parsed message or null if string content could not be parsed into an Mx
* @since 9.2.6
*/
public static AbstractMX parse(final String xml, MxId id, final MxReadConfiguration conf) {
return MxReadImpl.parse(xml, id, new MxReadParams(conf));
}
/**
* Used by subclasses to implement JSON deserialization.
*
* @param json a JSON representation of an MX message
* @param classOfT the specific MX subclass
* @return a specific deserialized MX message object
* @since 7.10.3
*/
protected static T fromJson(String json, Class classOfT) {
final Gson gson = getGsonBuilderWithV10Adapters();
return gson.fromJson(json, classOfT);
}
/**
* Creates an MX messages from its JSON representation.
*
* @param json a JSON representation of an MX message
* @return a specific deserialized MX message object, for example MxPain00100108
* @since 7.10.3
*/
public static AbstractMX fromJson(String json) {
final Gson gson = getGsonBuilderWithV10Adapters();
return gson.fromJson(json, AbstractMX.class);
}
/**
* Deserializes the given JSON string into an object of the specified class, using version 9 (Java 8) adapters.
*
* This method ensures compatibility by checking for date-time fields stored in JSON as
* {@link XMLGregorianCalendar} based json elements and converting them into {@link OffsetDateTime}. The Gson
* adapters (version 9) manage both Java 8 and Java 11 formats.
*
* @param the type of the object to be deserialized
* @param json a JSON representation of the object
* @param classOfT the class of the object to be deserialized
* @return a deserialized instance of the specified class, or null if the JSON string cannot be parsed.
* @since 10.1.8
*/
protected static T fromJsonV9(String json, Class classOfT) {
final Gson gson = getGsonBuilderWithV9Adapters();
return gson.fromJson(json, classOfT);
}
/**
* Deserializes the given JSON string into a specific MX message object, using version 9 (Java 8) adapters.
*
* This method ensures compatibility with older versions by converting
* {@link XMLGregorianCalendar} based json elements in JSON to {@link OffsetDateTime} if needed. The
* deserialization uses custom adapters (version 9) that handle both formats for date-time fields.
*
* @param json a JSON representation of an MX message
* @return a deserialized instance of {@link AbstractMX}, or null if the JSON string cannot be parsed.
* @since 10.1.8
*/
public static AbstractMX fromJsonV9(String json) {
final Gson gson = getGsonBuilderWithV9Adapters();
return gson.fromJson(json, AbstractMX.class);
}
/**
* 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 the configured 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 optional with prefixes as
* indicated in the configuration.
*
The configuration options enables customization of the XML serialization, including the root element name and
* prefixes. And it also provides default configurations for SWIFT and ISO 20022 envelopes.
*
* @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(MxWriteConfiguration conf) {
MxWriteConfiguration usableConf = conf != null ? conf : new MxWriteConfiguration();
MxWriteParams params = new MxWriteParams(usableConf);
// handle manually at this method level
params.includeXMLDeclaration = false;
EnvelopeType envelopeType = usableConf.envelopeType;
String envelopeElement =
envelopeType == EnvelopeType.CUSTOM ? usableConf.rootElement : envelopeType.rootElement();
StringBuilder xml = new StringBuilder();
if (usableConf.includeXMLDeclaration) {
xml.append("\n");
}
params.prefix = usableConf.headerPrefix;
final String header = header(params);
if (header != null) {
// open envelope element
xml.append("<");
if (envelopeType.prefix() != null) {
xml.append(envelopeType.prefix()).append(":");
}
xml.append(envelopeElement);
if (envelopeType != EnvelopeType.CUSTOM) {
xml.append(" xmlns=\"")
.append(usableConf.envelopeType.namespace())
.append("\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
}
xml.append(">\n");
// for ISO envelopes we have to add an extra element wrapping the header
if (envelopeType.name().startsWith("BME")) {
xml.append("<").append(envelopeType.prefix()).append(":Hdr>\n");
}
// write AppHdr
xml.append(header).append("\n");
// for ISO envelopes we have to close the extra element wrapping the header
if (envelopeType.name().startsWith("BME")) {
xml.append("").append(envelopeType.prefix()).append(":Hdr>\n");
}
}
// for ISO envelopes we have to wrap the Document in an extra element
if (envelopeType.name().startsWith("BME")) {
xml.append("<").append(envelopeType.prefix()).append(":Doc>\n");
}
// write Document
if (usableConf.documentPrefix != null && usableConf.useCategoryAsDocumentPrefix) {
params.prefix = getBusinessProcess();
} else {
params.prefix = usableConf.documentPrefix;
}
xml.append(document(params)).append("\n");
// for ISO envelopes we have to close the extra element wrapping the Document
if (envelopeType.name().startsWith("BME")) {
xml.append("").append(envelopeType.prefix()).append(":Doc>\n");
}
if (header != null) {
// close the envelope element
xml.append("");
if (envelopeType.prefix() != null) {
xml.append(envelopeType.prefix()).append(":");
}
xml.append(envelopeElement).append(">");
}
return xml.toString();
}
/**
* Get this message AppHdr as an XML string.
*
* @since 9.2.6
*/
public String header(final MxWriteParams params) {
if (this.appHdr != null) {
return this.appHdr.xml(params);
} else {
return null;
}
}
/**
* Get this message Document as an XML string.
*
* @param params not null marshalling parameters
* @since 9.2.6
*/
public String document(MxWriteParams params) {
Objects.requireNonNull(params, "marshalling params cannot be null");
return MxWriteImpl.write(getNamespace(), this, getClasses(), params);
}
/**
* Get the classes associated with this message
*
* @since 7.7
*/
@SuppressWarnings("rawtypes")
public abstract Class[] getClasses();
/**
* Get the XML namespace of the message
*
* @since 7.7
*/
public abstract String getNamespace();
/**
* 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 and content adapters.
*
* For more serialization options use {@link #header(MxWriteParams)}
*
* @return the serialized header or null if header is not set or errors occur during serialization
* @since 7.8
*/
public String header() {
return header(new MxWriteParams());
}
/**
* Get this message Document as an XML string.
*
*
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(MxWriteParams)}
*
* @return document serialized into XML string or null if errors occur during serialization
* @since 7.8
*/
public String document() {
MxWriteConfiguration conf = new MxWriteConfiguration();
MxWriteParams params = new MxWriteParams(conf);
params.prefix = conf.documentPrefix;
return document(params);
}
/**
* @deprecated use {@link #document(MxWriteParams)} instead
*/
@Deprecated
@ProwideDeprecated(phase4 = TargetYear.SRU2025)
public String document(final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
DeprecationUtils.phase3(
AbstractMX.class, "document(String, boolean, EscapeHandler)", "Use document(MxWriteParams) instead");
MxWriteParams params = new MxWriteParams();
params.prefix = prefix;
params.includeXMLDeclaration = includeXMLDeclaration;
params.escapeHandler = escapeHandler;
return document(params);
}
/**
* Convenience method to get this message XML as javax.xml.transform.Source.
*
* @return null if message() returns null or StreamSource in other case
* @see #message()
* @since 7.7
*/
public Source xmlSource() {
final String xml = message();
log.fine("XML: " + xml);
if (xml != null) {
return new StreamSource(new StringReader(xml));
}
return null;
}
/**
* Get this message as an XML string.
*
*
If the header is present, then 'AppHdr' and 'Document' elements will be wrapped under a default root element
* 'RequestPayload'. Both header and document are generated with the corresponding namespaces and by default the
* prefix 'head' is used for the header and the message category is used as prefix 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()}
*
* @return the XML content or null if errors occur during serialization
* @since 7.7
*/
@Override
public String message() {
return message(new MxWriteConfiguration());
}
/**
* @deprecated use {@link #message(MxWriteConfiguration)} and handle write from String to file with plain Java API
*/
@Deprecated
@ProwideDeprecated(phase4 = TargetYear.SRU2025)
public void write(final File file) throws IOException {
DeprecationUtils.phase3(AbstractMX.class, "write(File)", "Use message(MxWriteConfiguration) instead");
Objects.requireNonNull(file, "the file to write cannot be null");
boolean created = file.createNewFile();
if (created) {
log.fine("new file created: " + file.getAbsolutePath());
}
final FileOutputStream stream = new FileOutputStream(file.getAbsoluteFile());
write(stream);
stream.close();
}
/**
* @deprecated use {@link #message(MxWriteConfiguration)} and handle write from String to stream with plain Java API
*/
@Deprecated
@ProwideDeprecated(phase4 = TargetYear.SRU2025)
public void write(final OutputStream stream) throws IOException {
DeprecationUtils.phase3(AbstractMX.class, "write(OutputStream)", "Use message(MxWriteConfiguration) instead");
Objects.requireNonNull(stream, "the stream to write cannot be null");
stream.write(message().getBytes(StandardCharsets.UTF_8));
}
/**
* @return the business header or null if not set
* @since 9.0.1
*/
@XmlTransient
public AppHdr getAppHdr() {
return this.appHdr;
}
/**
* @param appHdr the header to set
* @since 9.0.1
*/
public void setAppHdr(final AppHdr appHdr) {
this.appHdr = appHdr;
}
/**
* Returns the MX message identification.
* Composed by the business process, functionality, variant and version.
*
* @return the constructed message id
* @since 7.7
*/
public MxId getMxId() {
MxId mxId = new MxId(
getBusinessProcess(),
StringUtils.leftPad(Integer.toString(getFunctionality()), 3, "0"),
StringUtils.leftPad(Integer.toString(getVariant()), 3, "0"),
StringUtils.leftPad(Integer.toString(getVersion()), 2, "0"));
if (this.appHdr != null) {
mxId.setBusinessService(this.appHdr.serviceName());
}
return mxId;
}
/**
* get the Alphabetic code in four positions (fixed length) identifying the Business Process
*
* @return the business process of the implementing class
* @since 7.7
*/
public abstract String getBusinessProcess();
/**
* Get the code identifying the Message Functionality
*
* @return the set functionality or null if not set
* @since 7.7
*/
public abstract int getFunctionality();
/**
* Get the Message variant
*
* @return the set variant or null if not set
* @since 7.7
*/
public abstract int getVariant();
/**
* Get the message version
*
* @return the set vesion or null if not set
* @since 7.7
*/
public abstract int getVersion();
public Element element() {
return element(null);
}
public Element element(JAXBContext inputContext) {
// it didn't work as expected
// properties.put(JAXBRIContext.DEFAULT_NAMESPACE_REMAP, namespace);
try {
JAXBContext context;
if (inputContext != null) {
context = inputContext;
} else {
context = JaxbContextLoader.INSTANCE.get(this.getClass(), getClasses());
}
DOMResult res = new DOMResult();
context.createMarshaller().marshal(this, res);
Document doc = (Document) res.getNode();
return (Element) doc.getFirstChild();
} catch (Exception e) {
log.log(Level.WARNING, "Error creating XML Document for MX", e);
return null;
}
}
/**
* Get a JSON representation of this MX message.
*
* @since 7.10.3
*/
@Override
public String toJson() {
// we use AbstractMX and not this.getClass() in order to force usage of the adapter
final Gson gson = getGsonBuilderWithV10Adapters();
return gson.toJson(this, AbstractMX.class);
}
/**
* Serializes this MX message into its JSON representation, using version 9 (Java 8) adapters.
*
*
This method ensures that when serializing to JSON, {@link OffsetDateTime} fields can be converted
* back into {@link XMLGregorianCalendar} based json element format for compatibility with older systems that still use the Java 8 format.
*
* @return a JSON representation of the MX message, compatible with Java 8
* @since 10.1.8
*/
public String toJsonV9() {
final Gson gson = getGsonBuilderWithV9Adapters();
return gson.toJson(this, AbstractMX.class);
}
private static GsonBuilder getGsonBuilderWithCustomAdapters() {
return new GsonBuilder()
.registerTypeAdapter(AbstractMX.class, new AbstractMXAdapter())
.registerTypeAdapter(OffsetTime.class, new OffsetTimeJsonAdapter())
.registerTypeAdapter(LocalDate.class, new LocalDateJsonAdapter())
.registerTypeAdapter(Year.class, new YearJsonAdapter())
.registerTypeAdapter(YearMonth.class, new YearMonthJsonAdapter())
.registerTypeAdapter(AppHdr.class, new AppHdrAdapter());
}
private static Gson getGsonBuilderWithV9Adapters() {
final Gson gson = getGsonBuilderWithCustomAdapters()
.registerTypeAdapter(OffsetDateTime.class, new V9DateTimeJsonAdapter())
.setPrettyPrinting()
.create();
return gson;
}
private static Gson getGsonBuilderWithV10Adapters() {
final Gson gson = getGsonBuilderWithCustomAdapters()
.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeJsonAdapter())
.setPrettyPrinting()
.create();
return gson;
}
/**
* @return same as {@link #getNamespace()}
* @since 9.1.2
*/
public String targetNamespace() {
return getNamespace();
}
}