This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/commons-secure-xml.git


The following commit(s) were added to refs/heads/main by this push:
     new 632cb04  Delegate the Java 18 XPathFactory property methods (#76)
632cb04 is described below

commit 632cb04d3ce06303387f370338cb942a719cef7a
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Tue Sep 1 13:48:31 2026 +0200

    Delegate the Java 18 XPathFactory property methods (#76)
    
    Java 18 added setProperty and getProperty to XPathFactory, with
    defaults that throw UnsupportedOperationException and an override in
    the JDK implementation that accepts its jdk.xml.xpath* limits. The
    wrapper is compiled against the Java 8 API, so it inherited those
    defaults and answered for the delegate: on Java 18 or later a caller
    could neither tighten nor read a limit through a secure factory, though
    nothing could be loosened either.
    
    Delegate both through method handles, the way newDefaultInstance
    already reaches a later-release static method. Neither carries
    @Override, which would not compile at release 8; each overrides at run
    time where the platform declares the method, and reports the inherited
    UnsupportedOperationException where it does not.
    
    The tests reach the pair reflectively for the same reason, which is
    also how a Java 18 caller resolves it, and skip on a platform without
    the methods.
    
    Assisted-By: Claude Fable 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01CLnTBsvmYtxzNTWVGNyz33
    
    Co-authored-by: Gary Gregory <[email protected]>
---
 src/changes/changes.xml                            |  1 +
 .../commons/xml/secure/MethodHandleFactory.java    | 22 +++++++++++
 .../commons/xml/secure/SecureXPathFactory.java     | 42 +++++++++++++++++++++
 .../commons/xml/secure/SecureXPathFactoryTest.java | 43 ++++++++++++++++++++++
 4 files changed, 108 insertions(+)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index c1a755b..5542ad0 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -48,6 +48,7 @@ The <action> type attribute can be add, update, fix, or 
remove.
       <action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Parse a Source opted in by a caller-supplied URIResolver using a 
secure parser.</action>
       <action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Secure the document parse behind the InputSource-taking XPath 
evaluation entry points.</action>
       <action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Fall back to the standard factory lookup in the DOM, SAX and schema 
newDefaultInstance methods on Android.</action>
+      <action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Delegate the XPathFactory setProperty and getProperty methods 
introduced in Java 18, so the implementation's properties stay reachable on a 
secure factory.</action>
       <action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Bound the content model a schema expands into, so a compact schema 
with a large maxOccurs cannot exhaust memory or CPU during validation.</action>
       <!-- UPDATE -->
       <action type="update" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory" 
issue="COMMONSXML-1,COMMONSXML-5,COMMONSXML-6,COMMONSXML-7,COMMONSXML-8">Recognize
 XML implementations by the JAXP features and properties they support instead 
of by their implementation class name, extending the securing to any compliant 
implementation.</action>
diff --git 
a/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java 
b/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java
index 75b8c99..d2fd64e 100644
--- a/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/MethodHandleFactory.java
@@ -49,6 +49,28 @@ static MethodHandle findStatic(final Class<?> 
refcAndReturnType, final String na
         }
     }
 
+    /**
+     * Finds an instance method handle for the given class, method name and 
signature.
+     *
+     * <p>Used to reach a method a later Java release added to a class this 
library compiles against at an earlier one; the handle is {@code null} where the
+     * running platform does not have it.</p>
+     *
+     * @param refc           the class to search for the method.
+     * @param name           the name of the method.
+     * @param returnType     the method's return type.
+     * @param parameterTypes the method's parameter types.
+     * @return the method handle, or {@code null} if not found.
+     * @throws SecurityException    if a security manager is present and it <a 
href="MethodHandles.Lookup.html#secmgr">refuses access</a>.
+     * @throws NullPointerException if any argument is null.
+     */
+    static MethodHandle findVirtual(final Class<?> refc, final String name, 
final Class<?> returnType, final Class<?>... parameterTypes) {
+        try {
+            return MethodHandles.publicLookup().findVirtual(refc, name, 
MethodType.methodType(returnType, parameterTypes));
+        } catch (final ReflectiveOperationException e) {
+            return null;
+        }
+    }
+
     static <T, E extends Throwable> T invokeExact(final ThrowableCallable<T> 
methodHandle, final Class<E> rethrow) throws E {
         try {
             return methodHandle.call();
diff --git 
a/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java 
b/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java
index 3c3aa9b..5724d1d 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java
@@ -74,6 +74,23 @@ public boolean getFeature(final String name) throws 
XPathFactoryConfigurationExc
             return delegate.getFeature(name);
         }
 
+        /**
+         * Reports a property of the delegate, the Java 18 {@code 
XPathFactory.getProperty(String)}.
+         *
+         * <p>Not marked {@code @Override}: this library compiles against the 
Java 8 API, where {@link XPathFactory} declares no such method, so the 
annotation
+         * would not compile. At run time on Java 18 or later it overrides the 
inherited method, which would otherwise answer for the wrapper and hide the
+         * delegate's own limits ({@code jdk.xml.xpath*}) behind an {@code 
UnsupportedOperationException}.</p>
+         *
+         * @param name the property name.
+         * @return the delegate's value for the property.
+         */
+        public String getProperty(final String name) {
+            if (MH_getProperty == null) {
+                throw new 
UnsupportedOperationException("XPathFactory.getProperty(String) requires Java 
18 or later");
+            }
+            return MethodHandleFactory.invokeExact(() -> (String) 
MH_getProperty.invokeExact(delegate, name), RuntimeException.class);
+        }
+
         @Override
         public boolean isObjectModelSupported(final String objectModel) {
             return delegate.isObjectModelSupported(objectModel);
@@ -107,6 +124,24 @@ public void setFeature(final String name, final boolean 
value) throws XPathFacto
             delegate.setFeature(name, value);
         }
 
+        /**
+         * Sets a property on the delegate, the Java 18 {@code 
XPathFactory.setProperty(String, String)}; see {@link #getProperty(String)} for 
why it carries no
+         * {@code @Override}. The {@code jdk.xml.xpath*} limits reached this 
way are processing limits like any other: an operator may tighten them, and
+         * loosening one is reconfiguration.
+         *
+         * @param name  the property name.
+         * @param value the value to set.
+         */
+        public void setProperty(final String name, final String value) {
+            if (MH_setProperty == null) {
+                throw new 
UnsupportedOperationException("XPathFactory.setProperty(String, String) 
requires Java 18 or later");
+            }
+            MethodHandleFactory.invokeExact(() -> {
+                MH_setProperty.invokeExact(delegate, name, value);
+                return null;
+            }, RuntimeException.class);
+        }
+
         @Override
         public void setXPathFunctionResolver(final XPathFunctionResolver 
resolver) {
             delegate.setXPathFunctionResolver(resolver);
@@ -123,6 +158,13 @@ public void setXPathVariableResolver(final 
XPathVariableResolver resolver) {
 
     private static final MethodHandle MH_newDefaultInstance = 
MethodHandleFactory.findStatic(XPathFactory.class, "newDefaultInstance");
 
+    /** {@code XPathFactory.getProperty(String)}, added in Java 18; {@code 
null} on earlier releases, where the method does not exist to be called. */
+    private static final MethodHandle MH_getProperty = 
MethodHandleFactory.findVirtual(XPathFactory.class, "getProperty", 
String.class, String.class);
+
+    /** {@code XPathFactory.setProperty(String, String)}, added in Java 18; 
{@code null} on earlier releases, where the method does not exist to be called. 
*/
+    private static final MethodHandle MH_setProperty =
+            MethodHandleFactory.findVirtual(XPathFactory.class, "setProperty", 
void.class, String.class, String.class);
+
     /**
      * Returns a new, secure {@link XPathFactory} of the system-default 
implementation, supporting the default XPath object model.
      * <p>
diff --git 
a/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java 
b/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java
index d6cbdc5..caaa5bd 100644
--- a/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java
+++ b/src/test/java/org/apache/commons/xml/secure/SecureXPathFactoryTest.java
@@ -17,18 +17,23 @@
 
 package org.apache.commons.xml.secure;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
 import javax.xml.xpath.XPath;
 import javax.xml.xpath.XPathFactory;
 import javax.xml.xpath.XPathFactoryConfigurationException;
 import javax.xml.xpath.XPathFunctionResolver;
 import javax.xml.xpath.XPathVariableResolver;
 
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 
@@ -122,4 +127,42 @@ public void setXPathVariableResolver(final 
XPathVariableResolver resolver) {
         };
         assertThrows(SecureException.class, () -> 
SecureXPathFactory.secure(rejectingFactory));
     }
+
+    /** A processing limit the JDK's XPath implementation recognizes through 
the Java 18 property API. */
+    private static final String XPATH_GROUP_LIMIT = 
"jdk.xml.xpathExprGrpLimit";
+
+    /**
+     * The Java 18 {@code XPathFactory} property method of the given name, or 
an aborted test where the platform predates it.
+     *
+     * <p>Reached reflectively because this suite compiles against the Java 8 
API, the same reason the wrapper delegates the pair through method handles: the
+     * call has to resolve at run time, which is also exactly how a Java 18 
caller reaches it.</p>
+     */
+    private static Method propertyMethod(final String name, final Class<?>... 
parameterTypes) {
+        try {
+            return XPathFactory.class.getMethod(name, parameterTypes);
+        } catch (final NoSuchMethodException e) {
+            Assumptions.abort("XPathFactory." + name + " requires Java 18 or 
later");
+            throw new AssertionError("unreachable");
+        }
+    }
+
+    @Test
+    void delegatesTheJava18PropertyApi() throws Exception {
+        // The wrapper is compiled against the Java 8 API, so without an 
explicit delegation the inherited default answers for it and every property the
+        // implementation supports, including its own limits, becomes 
unreachable through a secured factory.
+        final Method setProperty = propertyMethod("setProperty", String.class, 
String.class);
+        final Method getProperty = propertyMethod("getProperty", String.class);
+        final XPathFactory factory = SecureXPathFactory.newDefaultInstance();
+        setProperty.invoke(factory, XPATH_GROUP_LIMIT, "5");
+        assertEquals("5", getProperty.invoke(factory, XPATH_GROUP_LIMIT), "a 
property set on the secured factory must be read back from the delegate");
+    }
+
+    @Test
+    void reportsAnUnknownPropertyLikeTheDelegate() {
+        final Method getProperty = propertyMethod("getProperty", String.class);
+        final XPathFactory factory = SecureXPathFactory.newDefaultInstance();
+        final InvocationTargetException thrown = 
assertThrows(InvocationTargetException.class,
+                () -> getProperty.invoke(factory, "jdk.xml.noSuchProperty"));
+        assertInstanceOf(IllegalArgumentException.class, thrown.getCause(), 
"an unrecognized property must surface the delegate's own rejection");
+    }
 }

Reply via email to