diff --git a/client/.classpath b/client/.classpath index 8ea7380e16..7522075332 100644 --- a/client/.classpath +++ b/client/.classpath @@ -122,7 +122,6 @@ - @@ -213,7 +212,6 @@ - diff --git a/client/lib/xercesImpl-2.12.2.jar b/client/lib/xercesImpl-2.12.2.jar deleted file mode 100644 index ccbae9f456..0000000000 Binary files a/client/lib/xercesImpl-2.12.2.jar and /dev/null differ diff --git a/client/lib/xml-apis-1.4.01.jar b/client/lib/xml-apis-1.4.01.jar deleted file mode 100644 index 46733464fc..0000000000 Binary files a/client/lib/xml-apis-1.4.01.jar and /dev/null differ diff --git a/server/.classpath b/server/.classpath index 9ff5df7f09..aaac1b455c 100644 --- a/server/.classpath +++ b/server/.classpath @@ -269,7 +269,5 @@ - - diff --git a/server/docs/thirdparty/THIRD-PARTY-README.txt b/server/docs/thirdparty/THIRD-PARTY-README.txt index a1cc3d6442..ed5ff9b4ac 100644 --- a/server/docs/thirdparty/THIRD-PARTY-README.txt +++ b/server/docs/thirdparty/THIRD-PARTY-README.txt @@ -521,16 +521,6 @@ License: LGPL (dual-license with SPL) Use version 2.x as Xilize has a dependency on it; used only to produce documentation. -XML-APIs (extracted from Apache Xerces-2) -http://xerces.apache.org/xerces2-j/ -License: Apache v2 -We include the xml-apis.jar from the Xerces binary distribution in order to allow -our code to compile on JDK 1.4, which does not include newer XML APIs, even though -these API implementations will run on version 1.4 of the JRE. The JAR is unmodified -from the Xerces release, but is renamed as xml-apis-xerces-2.9.1.jar to -make the version clear. -Included as lib/xml-apis-xerces-2.9.1.jar - =================== End of License Information =================== @@ -883,74 +873,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- -The license below pertains to Apache Xerces2 Java (Build Tools) version 2.9.1, -which is included with Mirth Connect. - -=================== Beginning of License =================== - -/* - * The Apache Software License, Version 1.1 - * - * - * Copyright (c) 1999-2002 The Apache Software Foundation. All rights - * reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * 3. The end-user documentation included with the redistribution, - * if any, must include the following acknowledgment: - * "This product includes software developed by the - * Apache Software Foundation (http://www.apache.org/)." - * Alternately, this acknowledgment may appear in the software itself, - * if and wherever such third-party acknowledgments normally appear. - * - * 4. The names "Xerces" and "Apache Software Foundation" must - * not be used to endorse or promote products derived from this - * software without prior written permission. For written - * permission, please contact apache@apache.org. - * - * 5. Products derived from this software may not be called "Apache", - * nor may "Apache" appear in their name, without prior written - * permission of the Apache Software Foundation. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR - * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF - * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT - * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation and was - * originally based on software copyright (c) 1999, International - * Business Machines, Inc., http://www.ibm.com. For more - * information on the Apache Software Foundation, please see - * . - */ - -=================== End of License ========================= - - --------------------------------------------------------------------------------- - - The license below pertains to Jsch version 0.2.13, which is included with Mirth Connect. diff --git a/server/lib/xercesImpl-2.12.2.jar b/server/lib/xercesImpl-2.12.2.jar deleted file mode 100644 index ccbae9f456..0000000000 Binary files a/server/lib/xercesImpl-2.12.2.jar and /dev/null differ diff --git a/server/lib/xml-apis-1.4.01.jar b/server/lib/xml-apis-1.4.01.jar deleted file mode 100644 index 46733464fc..0000000000 Binary files a/server/lib/xml-apis-1.4.01.jar and /dev/null differ diff --git a/server/src/com/mirth/connect/plugins/datatypes/delimited/DelimitedReader.java b/server/src/com/mirth/connect/plugins/datatypes/delimited/DelimitedReader.java index 1cc0e4a45d..a3da25c9b9 100644 --- a/server/src/com/mirth/connect/plugins/datatypes/delimited/DelimitedReader.java +++ b/server/src/com/mirth/connect/plugins/datatypes/delimited/DelimitedReader.java @@ -1,11 +1,6 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Mirth Corporation +// SPDX-FileCopyrightText: 2025 Tony Germano package com.mirth.connect.plugins.datatypes.delimited; @@ -16,14 +11,13 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.xerces.parsers.SAXParser; -import org.xml.sax.ContentHandler; +import org.openintegrationengine.engine.plugins.datatypes.AbstractXMLReader; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.mirth.connect.util.StringUtil; -public class DelimitedReader extends SAXParser { +public class DelimitedReader extends AbstractXMLReader { private Logger logger = LogManager.getLogger(this.getClass()); private DelimitedSerializationProperties serializationProperties; @@ -49,8 +43,10 @@ public DelimitedReader(DelimitedSerializationProperties serializationProperties) ungottenRawText = null; } + @Override public void parse(InputSource input) throws SAXException, IOException { - + ensureHandlerSet(); + // Parsing overview // // The incoming stream is a single message which is a collection of one @@ -85,11 +81,10 @@ public void parse(InputSource input) throws SAXException, IOException { // Start the document String documentHead = "delimited"; - ContentHandler contentHandler = getContentHandler(); contentHandler.startDocument(); // Output - contentHandler.startElement("", documentHead, "", null); + contentHandler.startElement("", documentHead, "", getEmptyAttributes()); // While the parser gets records from the message ArrayList record; @@ -98,9 +93,9 @@ public void parse(InputSource input) throws SAXException, IOException { // Output if (serializationProperties.isNumberedRows()) { - contentHandler.startElement("", "row" + recordNo, "", null); + contentHandler.startElement("", "row" + recordNo, "", getEmptyAttributes()); } else { - contentHandler.startElement("", "row", "", null); + contentHandler.startElement("", "row", "", getEmptyAttributes()); } // For each column @@ -115,10 +110,11 @@ public void parse(InputSource input) throws SAXException, IOException { columnName = "column" + (i + 1); } // Output - contentHandler.startElement("", columnName, "", null); + contentHandler.startElement("", columnName, "", getEmptyAttributes()); // Output column value - contentHandler.characters(record.get(i).toCharArray(), 0, record.get(i).length()); + String val = record.get(i); + contentHandler.characters(val.toCharArray(), 0, val.length()); // Output contentHandler.endElement("", columnName, ""); diff --git a/server/src/com/mirth/connect/plugins/datatypes/edi/EDIReader.java b/server/src/com/mirth/connect/plugins/datatypes/edi/EDIReader.java index 8260653865..db55cc8590 100644 --- a/server/src/com/mirth/connect/plugins/datatypes/edi/EDIReader.java +++ b/server/src/com/mirth/connect/plugins/datatypes/edi/EDIReader.java @@ -1,11 +1,6 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Mirth Corporation +// SPDX-FileCopyrightText: 2025 Tony Germano package com.mirth.connect.plugins.datatypes.edi; @@ -15,19 +10,17 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.xerces.parsers.SAXParser; +import org.openintegrationengine.engine.plugins.datatypes.AbstractXMLReader; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; -public class EDIReader extends SAXParser { +public class EDIReader extends AbstractXMLReader { private Logger logger = LogManager.getLogger(this.getClass()); private String segmentDelimiter; - private String elementDelimiter; - private String subelementDelimiter; public EDIReader(String segmentDelimiter, String elementDelimiter, String subelementDelimiter) { @@ -37,7 +30,10 @@ public EDIReader(String segmentDelimiter, String elementDelimiter, String subele return; } + @Override public void parse(InputSource input) throws SAXException, IOException { + ensureHandlerSet(); + // Read the data from the InputSource BufferedReader in = new BufferedReader(input.getCharacterStream()); String nextLine = ""; @@ -78,13 +74,13 @@ public void parse(InputSource input) throws SAXException, IOException { documentHead = "EDIMessage"; } - AttributesImpl attributesImpl = new AttributesImpl(); + AttributesImpl attributesImpl = getEmptyAttributes(); attributesImpl.addAttribute("", "segmentDelimiter", "", "", segmentDelimiter); attributesImpl.addAttribute("", "elementDelimiter", "", "", elementDelimiter); attributesImpl.addAttribute("", "subelementDelimiter", "", "", subelementDelimiter); contentHandler.startElement("", documentHead, "", attributesImpl); } - contentHandler.startElement("", segmentID, "", null); + contentHandler.startElement("", segmentID, "", getEmptyAttributes()); int fieldID = 0; String field = "00"; @@ -101,7 +97,7 @@ public void parse(InputSource input) throws SAXException, IOException { // The naming is SEG. if (element.equals(elementDelimiter)) { if (lastsegElement) { - contentHandler.startElement("", segmentID + "." + field, "", null); + contentHandler.startElement("", segmentID + "." + field, "", getEmptyAttributes()); contentHandler.endElement("", segmentID + "." + field, ""); } fieldID++; @@ -110,7 +106,7 @@ public void parse(InputSource input) throws SAXException, IOException { lastsegElement = false; if (element.indexOf(subelementDelimiter) > -1) { - contentHandler.startElement("", segmentID + "." + field, "", null); + contentHandler.startElement("", segmentID + "." + field, "", getEmptyAttributes()); // check if we have sub-elements, if so add them StringTokenizer subelementTokenizer = new StringTokenizer(element, subelementDelimiter, true); subelementID = 1; @@ -120,7 +116,7 @@ public void parse(InputSource input) throws SAXException, IOException { if (subelement.equals(subelementDelimiter)) { String subelementName = segmentID + "." + field + "." + subelementID; if (lastsegSubelement) { - contentHandler.startElement("", subelementName, "", null); + contentHandler.startElement("", subelementName, "", getEmptyAttributes()); contentHandler.characters("".toCharArray(), 0, 0); contentHandler.endElement("", subelementName, ""); } @@ -132,7 +128,7 @@ public void parse(InputSource input) throws SAXException, IOException { lastsegSubelement = false; // The naming is SEG.. - contentHandler.startElement("", subelementName, "", null); + contentHandler.startElement("", subelementName, "", getEmptyAttributes()); contentHandler.characters(subelement.toCharArray(), 0, subelement.length()); contentHandler.endElement("", subelementName, ""); @@ -140,19 +136,19 @@ public void parse(InputSource input) throws SAXException, IOException { } String subelementName = segmentID + "." + (field) + "." + subelementID; if (lastsegSubelement) { - contentHandler.startElement("", subelementName, "", null); + contentHandler.startElement("", subelementName, "", getEmptyAttributes()); contentHandler.characters("".toCharArray(), 0, 0); contentHandler.endElement("", subelementName, ""); } - contentHandler.endElement("", segmentID + "." + (field), null); + contentHandler.endElement("", segmentID + "." + (field), ""); } else { - contentHandler.startElement("", segmentID + "." + field, "", null); - contentHandler.startElement("", segmentID + "." + field + ".1", "", null); + contentHandler.startElement("", segmentID + "." + field, "", getEmptyAttributes()); + contentHandler.startElement("", segmentID + "." + field + ".1", "", getEmptyAttributes()); // Set the text contents to the value contentHandler.characters(element.toCharArray(), 0, element.length()); - contentHandler.endElement("", segmentID + "." + (field) + ".1", null); - contentHandler.endElement("", segmentID + "." + (field), null); + contentHandler.endElement("", segmentID + "." + (field) + ".1", ""); + contentHandler.endElement("", segmentID + "." + (field), ""); } } @@ -162,7 +158,7 @@ public void parse(InputSource input) throws SAXException, IOException { // Set the field id here so we don't get dupe fields like // SE.01 and SE.01 when we have SE**~ field = fieldID < 10 ? "0" + fieldID : "" + fieldID; - contentHandler.startElement("", segmentID + "." + field, "", null); + contentHandler.startElement("", segmentID + "." + field, "", getEmptyAttributes()); contentHandler.endElement("", segmentID + "." + field, ""); } contentHandler.endElement("", segmentID, ""); @@ -175,5 +171,4 @@ public void parse(InputSource input) throws SAXException, IOException { contentHandler.endElement("", documentHead, ""); contentHandler.endDocument(); } - } diff --git a/server/src/com/mirth/connect/plugins/datatypes/hl7v2/ER7Reader.java b/server/src/com/mirth/connect/plugins/datatypes/hl7v2/ER7Reader.java index 0c1b4f7251..f6c68e6d35 100644 --- a/server/src/com/mirth/connect/plugins/datatypes/hl7v2/ER7Reader.java +++ b/server/src/com/mirth/connect/plugins/datatypes/hl7v2/ER7Reader.java @@ -1,11 +1,6 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Mirth Corporation +// SPDX-FileCopyrightText: 2025 Tony Germano package com.mirth.connect.plugins.datatypes.hl7v2; @@ -16,13 +11,14 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.xerces.parsers.SAXParser; +import org.openintegrationengine.engine.plugins.datatypes.AbstractXMLReader; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; -public class ER7Reader extends SAXParser { +public class ER7Reader extends AbstractXMLReader { private Logger logger = LogManager.getLogger(this.getClass()); + private boolean handleRepetitions = false; private boolean handleSubcomponents = false; private String segmentDelimiter; @@ -54,7 +50,10 @@ private String getMessageFromSource(InputSource source) throws IOException { return builder.toString().trim(); } + @Override public void parse(InputSource source) throws SAXException, IOException { + ensureHandlerSet(); + String message = getMessageFromSource(source); ContentHandler contentHandler = getContentHandler(); contentHandler.startDocument(); @@ -131,10 +130,10 @@ private String handleSegments(String documentHead, ContentHandler contentHandler if (segmentIndex == 0) { documentHead = MESSAGE_ROOT_ID; - contentHandler.startElement("", documentHead, "", null); + contentHandler.startElement("", documentHead, "", getEmptyAttributes()); } - contentHandler.startElement("", segmentId, "", null); + contentHandler.startElement("", segmentId, "", getEmptyAttributes()); handleFieldOrRepetitions(contentHandler, fieldSeparator, componentSeparator, subcomponentSeparator, repetitionSeparator, escapeCharacter, segmentId, fieldTokenizer); contentHandler.endElement("", segmentId, ""); } else { @@ -160,7 +159,7 @@ private void handleFieldOrRepetitions(ContentHandler contentHandler, String fiel // the naming is SEG. if (field.equals(fieldSeparator)) { if (atLastField) { - contentHandler.startElement("", segmentId + "." + fieldId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId, "", getEmptyAttributes()); contentHandler.endElement("", segmentId + "." + fieldId, ""); } @@ -176,11 +175,11 @@ private void handleFieldOrRepetitions(ContentHandler contentHandler, String fiel } if (enteredHeader && (fieldId == 1)) { - contentHandler.startElement("", segmentId + "." + fieldId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId, "", getEmptyAttributes()); contentHandler.characters(fieldSeparator.toCharArray(), 0, 1); - contentHandler.endElement("", segmentId + "." + (fieldId), null); + contentHandler.endElement("", segmentId + "." + (fieldId), ""); fieldId++; - contentHandler.startElement("", segmentId + "." + fieldId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId, "", getEmptyAttributes()); char[] specialCharacters; if (!subcomponentSeparator.isEmpty()) { @@ -196,7 +195,7 @@ private void handleFieldOrRepetitions(ContentHandler contentHandler, String fiel } contentHandler.characters(specialCharacters, 0, specialCharacters.length); - contentHandler.endElement("", segmentId + "." + (fieldId), null); + contentHandler.endElement("", segmentId + "." + (fieldId), ""); } else if (enteredHeader && (fieldId == 2)) { // do nothing } else { @@ -210,7 +209,7 @@ private void handleFieldOrRepetitions(ContentHandler contentHandler, String fiel } if (atLastField) { - contentHandler.startElement("", segmentId + "." + fieldId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId, "", getEmptyAttributes()); contentHandler.endElement("", segmentId + "." + fieldId, ""); } } @@ -225,7 +224,7 @@ private void handleFieldRepetitions(ContentHandler contentHandler, String compon if (field.equals(repetitionSeparator)) { // check for ~~ if (atLastRepetition) { - contentHandler.startElement("", segmentId + "." + fieldId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId, "", getEmptyAttributes()); contentHandler.characters(EMPTY_CHAR_ARRAY, 0, 0); contentHandler.endElement("", segmentId + "." + fieldId, ""); } @@ -239,7 +238,7 @@ private void handleFieldRepetitions(ContentHandler contentHandler, String compon } if (atLastRepetition) { - contentHandler.startElement("", segmentId + "." + fieldId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId, "", getEmptyAttributes()); contentHandler.characters(EMPTY_CHAR_ARRAY, 0, 0); contentHandler.endElement("", segmentId + "." + fieldId, ""); } @@ -247,17 +246,17 @@ private void handleFieldRepetitions(ContentHandler contentHandler, String compon private void handleField(ContentHandler contentHandler, String componentSeparator, String subcomponentSeparator, String segmentId, int fieldId, String field) throws SAXException { if ((field.indexOf(componentSeparator) > -1) || (handleSubcomponents && (field.indexOf(subcomponentSeparator) > -1))) { - contentHandler.startElement("", segmentId + "." + fieldId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId, "", getEmptyAttributes()); StringTokenizer componentTokenizer = new StringTokenizer(field, componentSeparator, true); handleComponents(contentHandler, componentSeparator, subcomponentSeparator, segmentId, fieldId, 1, componentTokenizer); - contentHandler.endElement("", segmentId + "." + fieldId, null); + contentHandler.endElement("", segmentId + "." + fieldId, ""); } else { logger.trace("handling field: " + field); - contentHandler.startElement("", segmentId + "." + fieldId, "", null); - contentHandler.startElement("", segmentId + "." + fieldId + ".1", "", null); + contentHandler.startElement("", segmentId + "." + fieldId, "", getEmptyAttributes()); + contentHandler.startElement("", segmentId + "." + fieldId + ".1", "", getEmptyAttributes()); contentHandler.characters(field.toCharArray(), 0, field.length()); - contentHandler.endElement("", segmentId + "." + fieldId + ".1", null); - contentHandler.endElement("", segmentId + "." + fieldId, null); + contentHandler.endElement("", segmentId + "." + fieldId + ".1", ""); + contentHandler.endElement("", segmentId + "." + fieldId, ""); } } @@ -269,7 +268,7 @@ private void handleComponents(ContentHandler contentHandler, String componentSep if (component.equals(componentSeparator)) { if (atLastComponent) { - contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId, "", getEmptyAttributes()); contentHandler.characters(EMPTY_CHAR_ARRAY, 0, 0); contentHandler.endElement("", segmentId + "." + fieldId + "." + componentId, ""); } @@ -283,7 +282,7 @@ private void handleComponents(ContentHandler contentHandler, String componentSep } if (atLastComponent) { - contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId, "", getEmptyAttributes()); contentHandler.characters(EMPTY_CHAR_ARRAY, 0, 0); contentHandler.endElement("", segmentId + "." + fieldId + "." + componentId, ""); } @@ -291,15 +290,15 @@ private void handleComponents(ContentHandler contentHandler, String componentSep private void handleComponent(ContentHandler contentHandler, String subcomponentSeparator, String segmentId, int fieldId, int componentId, String component) throws SAXException { if (handleSubcomponents && !subcomponentSeparator.isEmpty() && (component.indexOf(subcomponentSeparator) > -1)) { - contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId, "", getEmptyAttributes()); // check if we have subcomponents, if so add them StringTokenizer subcomponentTokenizer = new StringTokenizer(component, subcomponentSeparator, true); handleSubcomponents(contentHandler, subcomponentSeparator, segmentId, fieldId, componentId, 1, subcomponentTokenizer); - contentHandler.endElement("", segmentId + "." + fieldId + "." + componentId, null); + contentHandler.endElement("", segmentId + "." + fieldId + "." + componentId, ""); } else { logger.trace("handling component: " + component); // the naming is SEG.. - contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId, "", getEmptyAttributes()); contentHandler.characters(component.toCharArray(), 0, component.length()); contentHandler.endElement("", segmentId + "." + fieldId + "." + componentId, ""); } @@ -313,7 +312,7 @@ private void handleSubcomponents(ContentHandler contentHandler, String subcompon if (subcomponent.equals(subcomponentSeparator)) { if (atLastSubcomponent) { - contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId + "." + subcomponentId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId + "." + subcomponentId, "", getEmptyAttributes()); contentHandler.characters(EMPTY_CHAR_ARRAY, 0, 0); contentHandler.endElement("", segmentId + "." + fieldId + "." + componentId + "." + subcomponentId, ""); } @@ -324,14 +323,14 @@ private void handleSubcomponents(ContentHandler contentHandler, String subcompon logger.trace("handling subcomponent: " + subcomponentId); atLastSubcomponent = false; // the naming is SEG... - contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId + "." + subcomponentId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId + "." + subcomponentId, "", getEmptyAttributes()); contentHandler.characters(subcomponent.toCharArray(), 0, subcomponent.length()); contentHandler.endElement("", segmentId + "." + fieldId + "." + componentId + "." + subcomponentId, ""); } } if (atLastSubcomponent) { - contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId + "." + subcomponentId, "", null); + contentHandler.startElement("", segmentId + "." + fieldId + "." + componentId + "." + subcomponentId, "", getEmptyAttributes()); contentHandler.characters(EMPTY_CHAR_ARRAY, 0, 0); contentHandler.endElement("", segmentId + "." + fieldId + "." + componentId + "." + subcomponentId, ""); } diff --git a/server/src/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPReader.java b/server/src/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPReader.java index cc4a8388fb..6b86b0ed9e 100644 --- a/server/src/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPReader.java +++ b/server/src/com/mirth/connect/plugins/datatypes/ncpdp/NCPDPReader.java @@ -1,11 +1,6 @@ -/* - * Copyright (c) Mirth Corporation. All rights reserved. - * - * http://www.mirthcorp.com - * - * The software in this package is published under the terms of the MPL license a copy of which has - * been included with this distribution in the LICENSE.txt file. - */ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Mirth Corporation +// SPDX-FileCopyrightText: 2025 Tony Germano package com.mirth.connect.plugins.datatypes.ncpdp; @@ -16,15 +11,15 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.xerces.parsers.SAXParser; +import org.openintegrationengine.engine.plugins.datatypes.AbstractXMLReader; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; -public class NCPDPReader extends SAXParser { +public class NCPDPReader extends AbstractXMLReader { private Logger logger = LogManager.getLogger(this.getClass()); - + private String segmentDelimeter; private String groupDelimeter; private String fieldDelimeter; @@ -38,6 +33,8 @@ public NCPDPReader(String segmentDelimeter, String groupDelimeter, String fieldD @Override public void parse(InputSource input) throws SAXException, IOException { + ensureHandlerSet(); + // convert the InputSource to a String and trim the whitespace String message = IOUtils.toString(input.getCharacterStream()).trim(); @@ -84,11 +81,11 @@ public void parse(InputSource input) throws SAXException, IOException { if (firstTransaction) { firstTransaction = false; - contentHandler.startElement("", "TRANSACTIONS", "", null); + contentHandler.startElement("", "TRANSACTIONS", "", getEmptyAttributes()); } // process a group - AttributesImpl attr = new AttributesImpl(); + AttributesImpl attr = getEmptyAttributes(); attr.addAttribute("", "counter", "counter", "", Integer.toString(++groupCounter)); contentHandler.startElement("", "TRANSACTION", "", attr); inGroup = true; @@ -129,33 +126,33 @@ private String parseHeader(String message, ContentHandler contentHandler) throws version = header.substring(6, 8); headerElementName = "NCPDP_" + version + "_" + transactionName + "_Request"; - contentHandler.startElement("", headerElementName, "", null); - contentHandler.startElement("", "TransactionHeaderRequest", "", null); - contentHandler.startElement("", "BinNumber", "", null); + contentHandler.startElement("", headerElementName, "", getEmptyAttributes()); + contentHandler.startElement("", "TransactionHeaderRequest", "", getEmptyAttributes()); + contentHandler.startElement("", "BinNumber", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 0, 6); contentHandler.endElement("", "BinNumber", ""); - contentHandler.startElement("", "VersionReleaseNumber", "", null); + contentHandler.startElement("", "VersionReleaseNumber", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 6, 2); contentHandler.endElement("", "VersionReleaseNumber", ""); - contentHandler.startElement("", "TransactionCode", "", null); + contentHandler.startElement("", "TransactionCode", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 8, 2); contentHandler.endElement("", "TransactionCode", ""); - contentHandler.startElement("", "ProcessorControlNumber", "", null); + contentHandler.startElement("", "ProcessorControlNumber", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 10, 10); contentHandler.endElement("", "ProcessorControlNumber", ""); - contentHandler.startElement("", "TransactionCount", "", null); + contentHandler.startElement("", "TransactionCount", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 20, 1); contentHandler.endElement("", "TransactionCount", ""); - contentHandler.startElement("", "ServiceProviderIdQualifier", "", null); + contentHandler.startElement("", "ServiceProviderIdQualifier", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 21, 2); contentHandler.endElement("", "ServiceProviderIdQualifier", ""); - contentHandler.startElement("", "ServiceProviderId", "", null); + contentHandler.startElement("", "ServiceProviderId", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 23, 15); contentHandler.endElement("", "ServiceProviderId", ""); - contentHandler.startElement("", "DateOfService", "", null); + contentHandler.startElement("", "DateOfService", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 38, 8); contentHandler.endElement("", "DateOfService", ""); - contentHandler.startElement("", "SoftwareVendorCertificationId", "", null); + contentHandler.startElement("", "SoftwareVendorCertificationId", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 46, 10); contentHandler.endElement("", "SoftwareVendorCertificationId", ""); contentHandler.endElement("", "TransactionHeaderRequest", ""); @@ -164,27 +161,27 @@ private String parseHeader(String message, ContentHandler contentHandler) throws version = header.substring(0, 2); headerElementName = "NCPDP_" + version + "_" + transaction + "_Response"; - contentHandler.startElement("", headerElementName, "", null); - contentHandler.startElement("", "TransactionHeaderResponse", "", null); - contentHandler.startElement("", "VersionReleaseNumber", "", null); + contentHandler.startElement("", headerElementName, "", getEmptyAttributes()); + contentHandler.startElement("", "TransactionHeaderResponse", "", getEmptyAttributes()); + contentHandler.startElement("", "VersionReleaseNumber", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 0, 2); contentHandler.endElement("", "VersionReleaseNumber", ""); - contentHandler.startElement("", "TransactionCode", "", null); + contentHandler.startElement("", "TransactionCode", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 2, 2); contentHandler.endElement("", "TransactionCode", ""); - contentHandler.startElement("", "TransactionCount", "", null); + contentHandler.startElement("", "TransactionCount", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 4, 1); contentHandler.endElement("", "TransactionCount", ""); - contentHandler.startElement("", "HeaderResponseStatus", "", null); + contentHandler.startElement("", "HeaderResponseStatus", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 5, 1); contentHandler.endElement("", "HeaderResponseStatus", ""); - contentHandler.startElement("", "ServiceProviderIdQualifier", "", null); + contentHandler.startElement("", "ServiceProviderIdQualifier", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 6, 2); contentHandler.endElement("", "ServiceProviderIdQualifier", ""); - contentHandler.startElement("", "ServiceProviderId", "", null); + contentHandler.startElement("", "ServiceProviderId", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 8, 15); contentHandler.endElement("", "ServiceProviderId", ""); - contentHandler.startElement("", "DateOfService", "", null); + contentHandler.startElement("", "DateOfService", "", getEmptyAttributes()); contentHandler.characters(header.toCharArray(), 23, 8); contentHandler.endElement("", "DateOfService", ""); contentHandler.endElement("", "TransactionHeaderResponse", ""); @@ -222,7 +219,7 @@ private void parseSegment(String segment, ContentHandler contentHandler) throws subSegment = segment.substring(fieldDelimeterIndex + fieldDelimeter.length(), segment.length()); } - contentHandler.startElement("", NCPDPReference.getInstance().getSegment(segmentId, version), "", null); + contentHandler.startElement("", NCPDPReference.getInstance().getSegment(segmentId, version), "", getEmptyAttributes()); while (hasMoreFields) { fieldDelimeterIndex = subSegment.indexOf(fieldDelimeter); @@ -261,20 +258,20 @@ private void parseSegment(String segment, ContentHandler contentHandler) throws } inCounter = true; - AttributesImpl attr = new AttributesImpl(); + AttributesImpl attr = getEmptyAttributes(); attr.addAttribute("", "counter", "counter", "", fieldMessage); contentHandler.startElement("", fieldDescription, "", attr); fieldStack.push(fieldDescription); } else if (fieldDescription.endsWith("Count")) { // count field, add complex element inCount = true; - AttributesImpl attr = new AttributesImpl(); + AttributesImpl attr = getEmptyAttributes(); attr.addAttribute("", fieldDescription, fieldDescription, "", fieldMessage); // start the repeating field element contentHandler.startElement("", fieldDescription, "", attr); fieldStack.push(fieldDescription); } else { - contentHandler.startElement("", fieldDescription, "", null); + contentHandler.startElement("", fieldDescription, "", getEmptyAttributes()); contentHandler.characters(fieldMessage.toCharArray(), 0, fieldMessage.length()); contentHandler.endElement("", fieldDescription, ""); } @@ -291,4 +288,4 @@ private void parseSegment(String segment, ContentHandler contentHandler) throws private boolean isRepeatingField(String fieldDescription) { return NCPDPReference.getInstance().isRepeatingField(fieldDescription, version); } -} \ No newline at end of file +} diff --git a/server/src/org/openintegrationengine/engine/plugins/datatypes/AbstractXMLReader.java b/server/src/org/openintegrationengine/engine/plugins/datatypes/AbstractXMLReader.java new file mode 100644 index 0000000000..9a77f9514a --- /dev/null +++ b/server/src/org/openintegrationengine/engine/plugins/datatypes/AbstractXMLReader.java @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Tony Germano + +package org.openintegrationengine.engine.plugins.datatypes; + +import java.io.IOException; + +import org.xml.sax.ContentHandler; +import org.xml.sax.DTDHandler; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.AttributesImpl; + +/** + * A base class for custom XMLReaders used in data type plugins. + *

+ * This class handles the boilerplate requirements of the {@link XMLReader} interface, + * such as storing handler references and managing standard features/properties. + * Subclasses only need to implement the specific {@link #parse(InputSource)} logic. + *

+ */ +public abstract class AbstractXMLReader implements XMLReader { + + /** The ContentHandler to receive SAX events. */ + protected ContentHandler contentHandler; + + /** The ErrorHandler to receive error notifications. */ + protected ErrorHandler errorHandler; + + /** The DTDHandler to receive DTD events (rarely used for data types). */ + protected DTDHandler dtdHandler; + + /** The EntityResolver to resolve external entities. */ + protected EntityResolver entityResolver; + + /** Reusable instance that is cleared on each call of the accessor */ + private final AttributesImpl emptyAttributes = new AttributesImpl(); + + /** + * Helper method for subclasses to ensure the ContentHandler is configured. + *

+ * Subclasses should call this at the beginning of their {@code parse} method. + *

+ * + * @throws SAXException if the ContentHandler has not been set. + */ + protected void ensureHandlerSet() throws SAXException { + if (contentHandler == null) { + throw new SAXException("ContentHandler not set"); + } + } + + /** + * Helper method for subclasses to get an empty instance of {@link AttributesImpl}. + *

+ * This method reuses the same instance of {@link AttributesImpl} and clears it + * before use. + *

+ * + * @return An empty instance of {@link AttributesImpl} + */ + protected AttributesImpl getEmptyAttributes() { + emptyAttributes.clear(); + return emptyAttributes; + } + + /** + * Parse an XML document from the given input source. + *

+ * Subclasses must implement this method to translate their specific data format + * into SAX events on the {@code contentHandler}. + *

+ * + * @param input The input source for the top-level of the XML document. + * @throws IOException If an I/O error occurs. + * @throws SAXException If any SAX errors occur during processing. + */ + @Override + public abstract void parse(InputSource input) throws IOException, SAXException; + + /** + * Parse an XML document from a system identifier (URI). + *

+ * This implementation delegates to {@link #parse(InputSource)}. + *

+ * + * @param systemId The system identifier (URI). + * @throws IOException If an I/O error occurs. + * @throws SAXException If any SAX errors occur during processing. + */ + @Override + public void parse(String systemId) throws IOException, SAXException { + parse(new InputSource(systemId)); + } + + // --------------------------------------------------------- + // Standard Handler Getters/Setters + // --------------------------------------------------------- + + /** + * Return the current content handler. + * + * @return The current content handler, or null if none has been registered. + */ + @Override + public ContentHandler getContentHandler() { + return contentHandler; + } + + /** + * Allow an application to register a content event handler. + * + * @param handler The content handler. + */ + @Override + public void setContentHandler(ContentHandler handler) { + this.contentHandler = handler; + } + + /** + * Return the current error handler. + * + * @return The current error handler, or null if none has been registered. + */ + @Override + public ErrorHandler getErrorHandler() { + return errorHandler; + } + + /** + * Allow an application to register an error event handler. + * + * @param handler The error handler. + */ + @Override + public void setErrorHandler(ErrorHandler handler) { + this.errorHandler = handler; + } + + /** + * Return the current DTD handler. + * + * @return The current DTD handler, or null if none has been registered. + */ + @Override + public DTDHandler getDTDHandler() { + return dtdHandler; + } + + /** + * Allow an application to register a DTD event handler. + * + * @param handler The DTD handler. + */ + @Override + public void setDTDHandler(DTDHandler handler) { + this.dtdHandler = handler; + } + + /** + * Return the current entity resolver. + * + * @return The current entity resolver, or null if none has been registered. + */ + @Override + public EntityResolver getEntityResolver() { + return entityResolver; + } + + /** + * Allow an application to register an entity resolver. + * + * @param resolver The entity resolver. + */ + @Override + public void setEntityResolver(EntityResolver resolver) { + this.entityResolver = resolver; + } + + // --------------------------------------------------------- + // Strict Feature/Property Compliance + // --------------------------------------------------------- + + /** + * Look up the value of a feature flag. + *

+ * This implementation enforces mandatory SAX2 features: + *

    + *
  • {@code http://xml.org/sax/features/namespaces}: Always returns {@code true}.
  • + *
  • {@code http://xml.org/sax/features/namespace-prefixes}: Always returns {@code false}.
  • + *
+ * All other features throw {@link SAXNotRecognizedException}. + *

+ * + * @param name The feature name, which is a fully-qualified URI. + * @return The current value of the feature (true or false). + * @throws SAXNotRecognizedException If the feature value can't be assigned or + * retrieved. + * @throws SAXNotSupportedException When the feature name is recognized but its + * value cannot be determined at this time. + */ + @Override + public boolean getFeature(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + if ("http://xml.org/sax/features/namespaces".equals(name)) { + return true; + } + else if ("http://xml.org/sax/features/namespace-prefixes".equals(name)) { + return false; + } + throw new SAXNotRecognizedException("Feature not recognized: " + name); + } + + /** + * Set the value of a feature flag. + *

+ * This implementation prevents modifying mandatory SAX2 features to ensure the parser + * functions correctly with downstream transformers. + *

+ * + * @param name The feature name, which is a fully-qualified URI. + * @param value The requested value of the feature (true or false). + * @throws SAXNotRecognizedException If the feature value can't be assigned or retrieved. + * @throws SAXNotSupportedException When the feature name is recognized but its + * value cannot be set to the requested value. + */ + @Override + public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { + if ("http://xml.org/sax/features/namespaces".equals(name)) { + if (!value) { + throw new SAXNotSupportedException("Cannot disable 'namespaces' feature for this parser"); + } + return; + } + else if ("http://xml.org/sax/features/namespace-prefixes".equals(name)) { + if (value) { + throw new SAXNotSupportedException("Cannot enable 'namespace-prefixes' feature for this parser"); + } + return; + } + throw new SAXNotRecognizedException("Feature not recognized: " + name); + } + + /** + * Look up the value of a property. + * + * @param name The property name, which is a fully-qualified URI. + * @return The current value of the property. + * @throws SAXNotRecognizedException If the property value can't be assigned or retrieved. + * @throws SAXNotSupportedException When the property name is recognized but its + * value cannot be determined at this time. + */ + @Override + public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + throw new SAXNotRecognizedException("Property not recognized: " + name); + } + + /** + * Set the value of a property. + * + * @param name The property name, which is a fully-qualified URI. + * @param value The requested value for the property. + * @throws SAXNotRecognizedException If the property value can't be assigned or retrieved. + * @throws SAXNotSupportedException When the property name is recognized but its + * value cannot be set to the requested value. + */ + @Override + public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException { + throw new SAXNotRecognizedException("Property not recognized: " + name); + } +} diff --git a/server/test/org/openintegrationengine/engine/plugins/datatypes/AbstractXMLReaderTest.java b/server/test/org/openintegrationengine/engine/plugins/datatypes/AbstractXMLReaderTest.java new file mode 100644 index 0000000000..bff1664839 --- /dev/null +++ b/server/test/org/openintegrationengine/engine/plugins/datatypes/AbstractXMLReaderTest.java @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2025 Tony Germano + +package org.openintegrationengine.engine.plugins.datatypes; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.DTDHandler; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; + +public class AbstractXMLReaderTest { + + private AbstractXMLReader reader; + + @Before + public void setUp() { + reader = new TestXMLReader(); + } + + // --------------------------------------------------------- + // Parsing Logic Tests + // --------------------------------------------------------- + + @Test + public void testParseEmitsSaxEvents() throws Exception { + // Mock the ContentHandler to verify events + ContentHandler handler = mock(ContentHandler.class); + reader.setContentHandler(handler); + + // Execute parse + reader.parse(new InputSource()); + + // Verify the exact sequence of SAX events was fired + InOrder inOrder = inOrder(handler); + inOrder.verify(handler).startDocument(); + inOrder.verify(handler).startElement(eq(""), eq("test"), eq(""), any(Attributes.class)); + inOrder.verify(handler).characters(eq("value".toCharArray()), eq(0), eq(5)); + inOrder.verify(handler).endElement(eq(""), eq("test"), eq("")); + inOrder.verify(handler).endDocument(); + } + + @Test + public void testParseStringDelegatesToInputSource() throws IOException, SAXException { + // Spy on the reader to verify method calls + AbstractXMLReader spyReader = spy(new TestXMLReader()); + String testUri = "file:///test.xml"; + + // Set a dummy handler so ensuringHandlerSet doesn't fail + spyReader.setContentHandler(mock(ContentHandler.class)); + + spyReader.parse(testUri); + + // Verify that parse(String) called parse(InputSource) + verify(spyReader).parse(any(InputSource.class)); + } + + // --------------------------------------------------------- + // Handler Getter/Setter Tests + // --------------------------------------------------------- + + @Test + public void testContentHandlerAccessors() { + assertNull("Should be null initially", reader.getContentHandler()); + + ContentHandler mockHandler = mock(ContentHandler.class); + reader.setContentHandler(mockHandler); + + assertSame("Should return the set handler", mockHandler, reader.getContentHandler()); + } + + @Test + public void testErrorHandlerAccessors() { + assertNull("Should be null initially", reader.getErrorHandler()); + + ErrorHandler mockHandler = mock(ErrorHandler.class); + reader.setErrorHandler(mockHandler); + + assertSame("Should return the set handler", mockHandler, reader.getErrorHandler()); + } + + @Test + public void testDTDHandlerAccessors() { + assertNull("Should be null initially", reader.getDTDHandler()); + + DTDHandler mockHandler = mock(DTDHandler.class); + reader.setDTDHandler(mockHandler); + + assertSame("Should return the set handler", mockHandler, reader.getDTDHandler()); + } + + @Test + public void testEntityResolverAccessors() { + assertNull("Should be null initially", reader.getEntityResolver()); + + EntityResolver mockResolver = mock(EntityResolver.class); + reader.setEntityResolver(mockResolver); + + assertSame("Should return the set resolver", mockResolver, reader.getEntityResolver()); + } + + // --------------------------------------------------------- + // Helper Method Tests + // --------------------------------------------------------- + + @Test + public void testEnsureHandlerSetSuccess() throws SAXException { + // Setup: Set a handler + reader.setContentHandler(mock(ContentHandler.class)); + + // Execute: Should not throw exception + reader.ensureHandlerSet(); + } + + @Test(expected = SAXException.class) + public void testEnsureHandlerSetFailure() throws SAXException { + // Setup: Ensure handler is null + reader.setContentHandler(null); + + // Execute: Should throw SAXException + reader.ensureHandlerSet(); + } + + // --------------------------------------------------------- + // Feature Flag Tests + // --------------------------------------------------------- + + @Test + public void testGetFeatureNamespaces() throws Exception { + assertTrue("Namespaces should always be true", + reader.getFeature("http://xml.org/sax/features/namespaces")); + } + + @Test + public void testGetFeatureNamespacePrefixes() throws Exception { + assertFalse("Namespace prefixes should always be false", + reader.getFeature("http://xml.org/sax/features/namespace-prefixes")); + } + + @Test(expected = SAXNotRecognizedException.class) + public void testGetFeatureUnknown() throws Exception { + reader.getFeature("http://xml.org/sax/features/unknown-feature"); + } + + @Test + public void testSetFeatureNamespacesTrue() throws Exception { + // Should succeed (no-op) + reader.setFeature("http://xml.org/sax/features/namespaces", true); + } + + @Test(expected = SAXNotSupportedException.class) + public void testSetFeatureNamespacesFalse() throws Exception { + // Should fail - cannot disable namespaces + reader.setFeature("http://xml.org/sax/features/namespaces", false); + } + + @Test + public void testSetFeatureNamespacePrefixesFalse() throws Exception { + // Should succeed (no-op) + reader.setFeature("http://xml.org/sax/features/namespace-prefixes", false); + } + + @Test(expected = SAXNotSupportedException.class) + public void testSetFeatureNamespacePrefixesTrue() throws Exception { + // Should fail - cannot enable namespace prefixes + reader.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + } + + @Test(expected = SAXNotRecognizedException.class) + public void testSetFeatureUnknown() throws Exception { + reader.setFeature("http://xml.org/sax/features/unknown-feature", true); + } + + // --------------------------------------------------------- + // Property Tests + // --------------------------------------------------------- + + @Test(expected = SAXNotRecognizedException.class) + public void testGetPropertyUnknown() throws Exception { + reader.getProperty("http://xml.org/sax/properties/lexical-handler"); + } + + @Test(expected = SAXNotRecognizedException.class) + public void testSetPropertyUnknown() throws Exception { + reader.setProperty("http://xml.org/sax/properties/lexical-handler", new Object()); + } + + // --------------------------------------------------------- + // Concrete Implementation for Testing + // --------------------------------------------------------- + + /** + * A concrete implementation that simulates parsing a simple XML document: + * value + */ + private static class TestXMLReader extends AbstractXMLReader { + @Override + public void parse(InputSource input) throws IOException, SAXException { + // Verify handler is present + ensureHandlerSet(); + + // Simulate parsing value + contentHandler.startDocument(); + + contentHandler.startElement("", "test", "", getEmptyAttributes()); + + String text = "value"; + contentHandler.characters(text.toCharArray(), 0, text.length()); + + contentHandler.endElement("", "test", ""); + contentHandler.endDocument(); + } + } +}