From aceb908f9b6b2953fc838e89064026b0548eb2cc Mon Sep 17 00:00:00 2001 From: Jake Huneau Date: Sun, 16 Aug 2026 12:09:39 -0400 Subject: [PATCH] Don't fail on duplicate partnership --- .../partner/XMLPartnershipFactory.java | 107 +++++++++++++--- .../partner/XMLPartnershipFactoryTest.java | 117 ++++++++++++++++++ 2 files changed, 210 insertions(+), 14 deletions(-) create mode 100644 Server/src/test/java/org/openas2/partner/XMLPartnershipFactoryTest.java diff --git a/Server/src/main/java/org/openas2/partner/XMLPartnershipFactory.java b/Server/src/main/java/org/openas2/partner/XMLPartnershipFactory.java index 12a0c298..c5ea6dee 100644 --- a/Server/src/main/java/org/openas2/partner/XMLPartnershipFactory.java +++ b/Server/src/main/java/org/openas2/partner/XMLPartnershipFactory.java @@ -110,35 +110,57 @@ void loadPartnershipsFile() throws OpenAS2Exception { } void refreshConfig() throws OpenAS2Exception { - getSession().destroyPartnershipPollers(Session.PARTNERSHIP_POLLER); + Map newPartners = new HashMap(); + List newPartnerships = new ArrayList(); + /* Parse the whole file before touching the running pollers. Anything that goes wrong + * parsing then leaves the currently running configuration untouched rather than killing + * the pollers and aborting part way through the reload. + */ + List pollerNodes = new ArrayList(); + List pollerPartnerships = new ArrayList(); try { Element root = getPartnershipsXml().getDocumentElement(); NodeList rootNodes = root.getChildNodes(); Node rootNode; String nodeName; - Map newPartners = new HashMap(); - List newPartnerships = new ArrayList(); - for (int i = 0; i < rootNodes.getLength(); i++) { rootNode = rootNodes.item(i); nodeName = rootNode.getNodeName(); if (nodeName.equals("partner")) { - loadPartner(newPartners, rootNode); + loadPartner(newPartners, rootNode, false); } else if (nodeName.equals("partnership")) { - loadPartnership(newPartners, newPartnerships, rootNode); + Partnership partnership = loadPartnership(newPartners, newPartnerships, rootNode, false); + if (partnership != null) { + pollerNodes.add(rootNode); + pollerPartnerships.add(partnership); + } } } - - synchronized (this) { - setPartners(newPartners); - setPartnerships(newPartnerships); - } } catch (Exception e) { throw new WrappedException(e); } + + synchronized (this) { + setPartners(newPartners); + setPartnerships(newPartnerships); + } + + // The parsed config is good so now swap the pollers over to the new configuration + getSession().destroyPartnershipPollers(Session.PARTNERSHIP_POLLER); + for (int i = 0; i < pollerNodes.size(); i++) { + Partnership partnership = pollerPartnerships.get(i); + try { + setupPartnershipPoller(pollerNodes.get(i), partnership); + } catch (Exception e) { + /* A poller that cannot be configured must not prevent the rest of the partnerships + * from having their pollers started so report it and keep going. + */ + logger.error("Failed to configure the directory poller for partnership " + partnership.getName() + ": " + org.openas2.util.Logging.getExceptionMsg(e), e); + } + } } private void loadAttributes(Node node, Partnership partnership) throws OpenAS2Exception { @@ -149,13 +171,31 @@ private void loadAttributes(Node node, Partnership partnership) throws OpenAS2Ex } public void loadPartner(Map partners, Node node) throws OpenAS2Exception { + loadPartner(partners, node, true); + } + + /** + * Load a partner definition into the passed map of partners. + * + * @param partners - the map of partners the loaded partner is added to + * @param node - the XML node containing the partner definition + * @param failOnDuplicate - if true a partner that is already defined causes an exception, + * otherwise the duplicate is logged as an error and ignored so that + * the first definition found remains in effect + * @throws OpenAS2Exception - the partner definition could not be loaded + */ + public void loadPartner(Map partners, Node node, boolean failOnDuplicate) throws OpenAS2Exception { String[] requiredAttributes = {Partnership.PID_NAME}; Map newPartner = XMLUtil.mapAttributes(node, requiredAttributes); String name = newPartner.get(Partnership.PID_NAME); if (partners.get(name) != null) { - throw new OpenAS2Exception("Partner is defined more than once: " + name); + if (failOnDuplicate) { + throw new OpenAS2Exception("Partner is defined more than once: " + name); + } + logger.error("Partner is defined more than once in the partnerships file so the duplicate definition is ignored and the first one found is used: " + name); + return; } partners.put(name, newPartner); @@ -191,6 +231,27 @@ private void loadPartnerIDs(Map partners, String partnershipName } public void loadPartnership(Map partners, List partnerships, Node node) throws OpenAS2Exception { + Partnership partnership = loadPartnership(partners, partnerships, node, true); + if (partnership != null) { + setupPartnershipPoller(node, partnership); + } + } + + /** + * Load a partnership definition into the passed list of partnerships. This only parses the + * partnership. Use {@link #setupPartnershipPoller(Node, Partnership)} to activate any directory + * poller configured for it. + * + * @param partners - the map of partners the partnership sender and receiver are looked up in + * @param partnerships - the list of partnerships the loaded partnership is added to + * @param node - the XML node containing the partnership definition + * @param failOnDuplicate - if true a partnership that is already defined causes an exception, + * otherwise the duplicate is logged as an error and ignored so that + * the first definition found remains in effect + * @return the loaded partnership or null if it was ignored as a duplicate + * @throws OpenAS2Exception - the partnership definition could not be loaded + */ + public Partnership loadPartnership(Map partners, List partnerships, Node node, boolean failOnDuplicate) throws OpenAS2Exception { Partnership partnership = new Partnership(); String[] requiredAttributes = {"name"}; @@ -198,7 +259,11 @@ public void loadPartnership(Map partners, List part String name = psAttributes.get("name"); if (getPartnership(partnerships, name) != null) { - throw new OpenAS2Exception("Partnership is defined more than once: " + name); + if (failOnDuplicate) { + throw new OpenAS2Exception("Partnership is defined more than once: " + name); + } + logger.error("Partnership is defined more than once in the partnerships file so the duplicate definition is ignored and the first one found is used: " + name); + return null; } partnership.setName(name); @@ -220,7 +285,21 @@ public void loadPartnership(Map partners, List part } // add the partnership to the list of available partnerships partnerships.add(partnership); - + + return partnership; + } + + /** + * Activate the directory polling module configured for a partnership, if there is one. + * Any poller currently running for the partnership must have been destroyed before calling + * this otherwise the polled directory will be rejected as already in use. + * + * @param node - the XML node containing the partnership definition + * @param partnership - the partnership the poller belongs to + * @throws OpenAS2Exception - the poller could not be configured + */ + public void setupPartnershipPoller(Node node, Partnership partnership) throws OpenAS2Exception { + String name = partnership.getName(); // Now check if we need to add a directory polling module Node pollerCfgNode = XMLUtil.findChildNode(node, Partnership.PCFG_POLLER); if (pollerCfgNode != null) { diff --git a/Server/src/test/java/org/openas2/partner/XMLPartnershipFactoryTest.java b/Server/src/test/java/org/openas2/partner/XMLPartnershipFactoryTest.java new file mode 100644 index 00000000..21a840c8 --- /dev/null +++ b/Server/src/test/java/org/openas2/partner/XMLPartnershipFactoryTest.java @@ -0,0 +1,117 @@ +package org.openas2.partner; + +import org.junit.jupiter.api.Test; +import org.openas2.OpenAS2Exception; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilderFactory; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Verifies how duplicate definitions in the partnerships file are handled. A duplicate must not + * abort loading the file since that leaves the system unable to send anything, but the commands + * that add a partner or partnership must still reject one that already exists. + */ +public class XMLPartnershipFactoryTest { + + private final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + + public XMLPartnershipFactoryTest() throws Exception { + } + + private Element partnerNode(String name, String as2Id) { + Element partner = doc.createElement("partner"); + partner.setAttribute(Partnership.PID_NAME, name); + partner.setAttribute(Partnership.PID_AS2, as2Id); + return partner; + } + + private Element partnershipNode(String name, String senderName, String receiverName, String url) { + Element partnership = doc.createElement("partnership"); + partnership.setAttribute("name", name); + Element sender = doc.createElement(Partnership.PTYPE_SENDER); + sender.setAttribute(Partnership.PID_NAME, senderName); + partnership.appendChild(sender); + Element receiver = doc.createElement(Partnership.PTYPE_RECEIVER); + receiver.setAttribute(Partnership.PID_NAME, receiverName); + partnership.appendChild(receiver); + Element attribute = doc.createElement("attribute"); + attribute.setAttribute("name", Partnership.PA_AS2_URL); + attribute.setAttribute("value", url); + partnership.appendChild(attribute); + return partnership; + } + + private Map partners() throws OpenAS2Exception { + XMLPartnershipFactory factory = new XMLPartnershipFactory(); + Map partners = new HashMap(); + factory.loadPartner(partners, partnerNode("MyCompany", "MyCompany_OID"), false); + factory.loadPartner(partners, partnerNode("PartnerA", "PartnerA_OID"), false); + return partners; + } + + @Test + public void duplicatePartnershipIsIgnoredAndTheFirstOneIsUsed() throws Exception { + XMLPartnershipFactory factory = new XMLPartnershipFactory(); + Map partners = partners(); + List partnerships = new ArrayList(); + + assertNotNull(factory.loadPartnership(partners, partnerships, + partnershipNode("MyCompany-to-PartnerA", "MyCompany", "PartnerA", "http://first:10080"), false)); + assertNull(factory.loadPartnership(partners, partnerships, + partnershipNode("MyCompany-to-PartnerA", "MyCompany", "PartnerA", "http://second:10080"), false), + "the duplicate partnership must be reported as skipped"); + + assertEquals(1, partnerships.size(), "the duplicate must not be added to the partnership list"); + assertEquals("http://first:10080", partnerships.get(0).getAttribute(Partnership.PA_AS2_URL), + "the first definition found must remain in effect"); + } + + @Test + public void duplicatePartnerIsIgnoredAndTheFirstOneIsUsed() throws Exception { + XMLPartnershipFactory factory = new XMLPartnershipFactory(); + Map partners = new HashMap(); + + factory.loadPartner(partners, partnerNode("PartnerA", "PartnerA_OID"), false); + factory.loadPartner(partners, partnerNode("PartnerA", "SomeOtherOID"), false); + + assertEquals(1, partners.size()); + @SuppressWarnings("unchecked") + Map partner = (Map) partners.get("PartnerA"); + assertEquals("PartnerA_OID", partner.get(Partnership.PID_AS2), + "the first definition found must remain in effect"); + } + + @Test + public void addingAnAlreadyDefinedPartnershipStillFails() throws Exception { + XMLPartnershipFactory factory = new XMLPartnershipFactory(); + Map partners = partners(); + List partnerships = new ArrayList(); + + factory.loadPartnership(partners, partnerships, + partnershipNode("MyCompany-to-PartnerA", "MyCompany", "PartnerA", "http://first:10080")); + + assertThrows(OpenAS2Exception.class, () -> factory.loadPartnership(partners, partnerships, + partnershipNode("MyCompany-to-PartnerA", "MyCompany", "PartnerA", "http://second:10080"))); + } + + @Test + public void addingAnAlreadyDefinedPartnerStillFails() throws Exception { + XMLPartnershipFactory factory = new XMLPartnershipFactory(); + Map partners = new HashMap(); + + factory.loadPartner(partners, partnerNode("PartnerA", "PartnerA_OID")); + + assertThrows(OpenAS2Exception.class, + () -> factory.loadPartner(partners, partnerNode("PartnerA", "SomeOtherOID"))); + } +}