This is an automated email from the ASF dual-hosted git repository. paulk pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 59a9b97d672a4bed3a87f84e6de6f894053a6411 Author: Paul King <[email protected]> AuthorDate: Mon Apr 13 13:55:55 2026 +1000 GROOVY-7633: Add constructor with Map for SAXOptions to XmlSlurper --- .../src/main/java/groovy/xml/XmlSlurper.java | 150 ++++++++++++++++++--- .../test/groovy/groovy/xml/XmlSlurperTest.groovy | 26 ++++ 2 files changed, 154 insertions(+), 22 deletions(-) diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/XmlSlurper.java b/subprojects/groovy-xml/src/main/java/groovy/xml/XmlSlurper.java index 3925a53352..9236c94ff1 100644 --- a/subprojects/groovy-xml/src/main/java/groovy/xml/XmlSlurper.java +++ b/subprojects/groovy-xml/src/main/java/groovy/xml/XmlSlurper.java @@ -85,22 +85,29 @@ import static groovy.xml.XmlUtil.setFeatureQuietly; * @see GPathResult */ public class XmlSlurper extends DefaultHandler { - private final XMLReader reader; + private XMLReader reader; private Node currentNode = null; private final Stack<Node> stack = new Stack<>(); private final StringBuilder charBuffer = new StringBuilder(); private final Map<String, String> namespaceTagHints = new HashMap<>(); private boolean keepIgnorableWhitespace = false; - private boolean namespaceAware = false; + private boolean namespaceAware = true; + private boolean validating = false; + private boolean allowDocTypeDeclaration = false; /** * Creates a non-validating and namespace-aware <code>XmlSlurper</code> which does not allow DOCTYPE declarations in documents. + * <p> + * Parser options can be configured via setters before the first parse call: + * <pre> + * // Using Groovy named parameters: + * def slurper = new XmlSlurper(namespaceAware: false, keepIgnorableWhitespace: true) + * </pre> * * @throws ParserConfigurationException if no parser which satisfies the requested configuration can be created. * @throws SAXException for SAX errors. */ public XmlSlurper() throws ParserConfigurationException, SAXException { - this(false, true); } /** @@ -127,21 +134,47 @@ public class XmlSlurper extends DefaultHandler { * @throws SAXException for SAX errors. */ public XmlSlurper(final boolean validating, final boolean namespaceAware, boolean allowDocTypeDeclaration) throws ParserConfigurationException, SAXException { + this.validating = validating; + this.namespaceAware = namespaceAware; + this.allowDocTypeDeclaration = allowDocTypeDeclaration; + } + + public XmlSlurper(final XMLReader reader) { + this.reader = reader; + } + + public XmlSlurper(final SAXParser parser) throws SAXException { + this(parser.getXMLReader()); + } + + private void initReader() throws ParserConfigurationException, SAXException { SAXParserFactory factory = FactorySupport.createSaxParserFactory(); factory.setNamespaceAware(namespaceAware); - this.namespaceAware = namespaceAware; factory.setValidating(validating); setFeatureQuietly(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); setFeatureQuietly(factory, "http://apache.org/xml/features/disallow-doctype-decl", !allowDocTypeDeclaration); reader = factory.newSAXParser().getXMLReader(); } - public XmlSlurper(final XMLReader reader) { - this.reader = reader; + private XMLReader getReader() throws ParserConfigurationException, SAXException { + if (reader == null) { + initReader(); + } + return reader; } - public XmlSlurper(final SAXParser parser) throws SAXException { - this(parser.getXMLReader()); + private XMLReader ensureReader() { + try { + return getReader(); + } catch (ParserConfigurationException | SAXException e) { + throw new RuntimeException("Failed to initialize XML reader", e); + } + } + + private void checkNotInitialized(String property) { + if (reader != null) { + throw new IllegalStateException(property + " must be set before parsing"); + } } /** @@ -169,6 +202,74 @@ public class XmlSlurper extends DefaultHandler { return keepIgnorableWhitespace; } + /** + * Determine if namespace handling is enabled. + * + * @return true if namespace handling is enabled + */ + public boolean isNamespaceAware() { + return namespaceAware; + } + + /** + * Enable and/or disable namespace handling. + * Must be set before the first parse call. + * + * @param namespaceAware the new desired value + * @throws IllegalStateException if called after parsing has started + * @since 6.0.0 + */ + public void setNamespaceAware(boolean namespaceAware) { + checkNotInitialized("namespaceAware"); + this.namespaceAware = namespaceAware; + } + + /** + * Determine if the parser validates documents. + * + * @return true if validation is enabled + * @since 6.0.0 + */ + public boolean isValidating() { + return validating; + } + + /** + * Enable and/or disable validation. + * Must be set before the first parse call. + * + * @param validating the new desired value + * @throws IllegalStateException if called after parsing has started + * @since 6.0.0 + */ + public void setValidating(boolean validating) { + checkNotInitialized("validating"); + this.validating = validating; + } + + /** + * Determine if DOCTYPE declarations are allowed. + * + * @return true if DOCTYPE declarations are allowed + * @since 6.0.0 + */ + public boolean isAllowDocTypeDeclaration() { + return allowDocTypeDeclaration; + } + + /** + * Enable and/or disable DOCTYPE declaration support. + * Must be set before the first parse call. + * + * @param allowDocTypeDeclaration the new desired value + * @throws IllegalStateException if called after parsing has started + * @since 6.0.0 + */ + public void setAllowDocTypeDeclaration(boolean allowDocTypeDeclaration) { + checkNotInitialized("allowDocTypeDeclaration"); + this.allowDocTypeDeclaration = allowDocTypeDeclaration; + } + /** * @return The GPathResult instance created by consuming a stream of SAX events * Note if one of the parse methods has been called then this returns null @@ -196,9 +297,14 @@ public class XmlSlurper extends DefaultHandler { * or character stream supplied by the application. */ public GPathResult parse(final InputSource input) throws IOException, SAXException { - reader.setContentHandler(this); - reader.parse(input); - return getDocument(); + try { + XMLReader r = getReader(); + r.setContentHandler(this); + r.parse(input); + return getDocument(); + } catch (ParserConfigurationException e) { + throw new SAXException(e); + } } /** @@ -299,49 +405,49 @@ public class XmlSlurper extends DefaultHandler { * @see org.xml.sax.XMLReader#getDTDHandler() */ public DTDHandler getDTDHandler() { - return reader.getDTDHandler(); + return ensureReader().getDTDHandler(); } /* (non-Javadoc) * @see org.xml.sax.XMLReader#getEntityResolver() */ public EntityResolver getEntityResolver() { - return reader.getEntityResolver(); + return ensureReader().getEntityResolver(); } /* (non-Javadoc) * @see org.xml.sax.XMLReader#getErrorHandler() */ public ErrorHandler getErrorHandler() { - return reader.getErrorHandler(); + return ensureReader().getErrorHandler(); } /* (non-Javadoc) * @see org.xml.sax.XMLReader#getFeature(java.lang.String) */ public boolean getFeature(final String uri) throws SAXNotRecognizedException, SAXNotSupportedException { - return reader.getFeature(uri); + return ensureReader().getFeature(uri); } /* (non-Javadoc) * @see org.xml.sax.XMLReader#getProperty(java.lang.String) */ public Object getProperty(final String uri) throws SAXNotRecognizedException, SAXNotSupportedException { - return reader.getProperty(uri); + return ensureReader().getProperty(uri); } /* (non-Javadoc) * @see org.xml.sax.XMLReader#setDTDHandler(org.xml.sax.DTDHandler) */ public void setDTDHandler(final DTDHandler dtdHandler) { - reader.setDTDHandler(dtdHandler); + ensureReader().setDTDHandler(dtdHandler); } /* (non-Javadoc) * @see org.xml.sax.XMLReader#setEntityResolver(org.xml.sax.EntityResolver) */ public void setEntityResolver(final EntityResolver entityResolver) { - reader.setEntityResolver(entityResolver); + ensureReader().setEntityResolver(entityResolver); } /** @@ -350,7 +456,7 @@ public class XmlSlurper extends DefaultHandler { * @param base The URL used to resolve relative URLs */ public void setEntityBaseUrl(final URL base) { - reader.setEntityResolver((publicId, systemId) -> { + ensureReader().setEntityResolver((publicId, systemId) -> { try { return new InputSource(base.toURI().resolve(systemId).toURL().openStream()); } catch (URISyntaxException e) { @@ -363,21 +469,21 @@ public class XmlSlurper extends DefaultHandler { * @see org.xml.sax.XMLReader#setErrorHandler(org.xml.sax.ErrorHandler) */ public void setErrorHandler(final ErrorHandler errorHandler) { - reader.setErrorHandler(errorHandler); + ensureReader().setErrorHandler(errorHandler); } /* (non-Javadoc) * @see org.xml.sax.XMLReader#setFeature(java.lang.String, boolean) */ public void setFeature(final String uri, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { - reader.setFeature(uri, value); + ensureReader().setFeature(uri, value); } /* (non-Javadoc) * @see org.xml.sax.XMLReader#setProperty(java.lang.String, java.lang.Object) */ public void setProperty(final String uri, final Object value) throws SAXNotRecognizedException, SAXNotSupportedException { - reader.setProperty(uri, value); + ensureReader().setProperty(uri, value); } // ContentHandler interface diff --git a/subprojects/groovy-xml/src/test/groovy/groovy/xml/XmlSlurperTest.groovy b/subprojects/groovy-xml/src/test/groovy/groovy/xml/XmlSlurperTest.groovy index 63d4e13beb..f12863b7a8 100644 --- a/subprojects/groovy-xml/src/test/groovy/groovy/xml/XmlSlurperTest.groovy +++ b/subprojects/groovy-xml/src/test/groovy/groovy/xml/XmlSlurperTest.groovy @@ -299,4 +299,30 @@ final class XmlSlurperTest { assert xml.message.part.lookupNamespace("undefinedPrefix") == null xml.message.findAll { true }.each { assert it.name() == "message"} } + + @Test + void testNamedParameterConstruction() { + def xml = '<root><item name="test">value</item></root>' + + // named parameters via setters + def slurper = new XmlSlurper(namespaceAware: false, keepIgnorableWhitespace: true) + def result = slurper.parseText(xml) + assert result.item.text() == 'value' + assert result.item.@name == 'test' + + // setter after boolean constructor, before first parse + def slurper2 = new XmlSlurper(false, false) + slurper2.allowDocTypeDeclaration = true + assert slurper2.parseText(xml).item.text() == 'value' + + // setter after parse should fail + def slurper3 = new XmlSlurper() + slurper3.parseText(xml) + try { + slurper3.namespaceAware = false + assert false, 'should have thrown' + } catch (IllegalStateException e) { + assert e.message.contains('must be set before parsing') + } + } }
