This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/errors in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit 78691945844bbf32f4be2241c93bea8760d3b01c Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Sep 17 09:24:17 2026 +0100 Keep document-walk failures distinguishable --- .../schema/docpath/XmlSchemaPathFinder.java | 55 +++++--- .../schema/docpath/TestPathFinderFailureTypes.java | 146 +++++++++++++++++++++ 2 files changed, 185 insertions(+), 16 deletions(-) diff --git a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java index e92184e2..0fa67992 100644 --- a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java +++ b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java @@ -67,10 +67,17 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { * cross-product of choices. Both bounds are configurable via system * properties. */ - private static final int MAX_DECISION_POINTS = - getIntProperty("org.apache.ws.commons.schema.walker.maxDecisionPoints", 10000); - private static final long MAX_REPLAYED_EVENTS = - getIntProperty("org.apache.ws.commons.schema.walker.maxReplayedEvents", 1000000); + private static final String MAX_DECISION_POINTS_PROPERTY = + "org.apache.ws.commons.schema.walker.maxDecisionPoints"; + private static final String MAX_REPLAYED_EVENTS_PROPERTY = + "org.apache.ws.commons.schema.walker.maxReplayedEvents"; + + // Read per instance: a static final is read once per class load, so setting either + // property had no effect on a JVM that had already touched this class. + private final int maxDecisionPoints = + getIntProperty(MAX_DECISION_POINTS_PROPERTY, 10000); + private final long maxReplayedEvents = + getIntProperty(MAX_REPLAYED_EVENTS_PROPERTY, 1000000); private final XmlSchemaNamespaceContext nsContext; @@ -447,25 +454,41 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { decisionPoints = null; // Hopefully there won't be any! } + /** + * Keeps the outcomes a caller can act on - an invalid document, an unusable facet, an + * exhausted budget - distinguishable from an internal error, which stays a + * {@link RuntimeException}. {@link ValidationException} is checked, so it is carried as a + * cause rather than rethrown. + */ + private static RuntimeException reportable(String context, Exception e) { + if (e instanceof XmlSchemaException) { + return (XmlSchemaException)e; + } + if (e instanceof ValidationException) { + return new XmlSchemaException(context, e); + } + return new RuntimeException(context, e); + } + private void recordDecisionPoint() { ++decisionPointCount; - if (decisionPointCount > MAX_DECISION_POINTS) { - throw new XmlSchemaException("More than " + MAX_DECISION_POINTS + if (decisionPointCount > maxDecisionPoints) { + throw new XmlSchemaException("More than " + maxDecisionPoints + " decision points were created while matching this document; the schema" + " likely contains ambiguous (Unique Particle Attribution violating)" + " content models. The limit may be changed with the" - + " org.apache.ws.commons.schema.walker.maxDecisionPoints system property."); + + " " + MAX_DECISION_POINTS_PROPERTY + " system property."); } } private void recordReplayedEvent() { ++replayedEventCount; - if (replayedEventCount > MAX_REPLAYED_EVENTS) { - throw new XmlSchemaException("More than " + MAX_REPLAYED_EVENTS + if (replayedEventCount > maxReplayedEvents) { + throw new XmlSchemaException("More than " + maxReplayedEvents + " traversed elements were replayed while backtracking through this" + " document; the schema likely contains ambiguous (Unique Particle" + " Attribution violating) content models. The limit may be changed with the" - + " org.apache.ws.commons.schema.walker.maxReplayedEvents system property."); + + " " + MAX_REPLAYED_EVENTS_PROPERTY + " system property."); } } @@ -801,8 +824,8 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { * internal exception is thrown instead. Likewise, any useful info * about the error reported in the wrapper SAXException is lost. */ - throw new RuntimeException("Error occurred while starting element " + elemQName - + "; traversed path is " + getElementsTraversedAsString(), e); + throw reportable("Error occurred while starting element " + elemQName + + "; traversed path is " + getElementsTraversedAsString(), e); } } @@ -874,8 +897,8 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { .add(new TraversedElement(element.getQName(), TraversedElement.Traversal.CONTENT)); } catch (Exception e) { - throw new RuntimeException("Error occurred while processing characters; traversed path was " - + getElementsTraversedAsString(), e); + throw reportable("Error occurred while processing characters; traversed path was " + + getElementsTraversedAsString(), e); } } @@ -944,8 +967,8 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { } } catch (Exception e) { - throw new RuntimeException("Error occurred while ending element " + elemQName - + "; traversed path was " + getElementsTraversedAsString(), e); + throw reportable("Error occurred while ending element " + elemQName + + "; traversed path was " + getElementsTraversedAsString(), e); } } diff --git a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/docpath/TestPathFinderFailureTypes.java b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/docpath/TestPathFinderFailureTypes.java new file mode 100644 index 00000000..7aaec5c0 --- /dev/null +++ b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/docpath/TestPathFinderFailureTypes.java @@ -0,0 +1,146 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ws.commons.schema.docpath; + +import java.io.StringReader; + +import javax.xml.bind.ValidationException; +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.ws.commons.schema.XmlSchemaCollection; +import org.apache.ws.commons.schema.XmlSchemaException; +import org.apache.ws.commons.schema.walker.XmlSchemaWalker; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.xml.sax.InputSource; + +/** + * A failure during the document walk must stay distinguishable from an internal error, which + * reporting everything as a bare RuntimeException did not allow. + */ +public class TestPathFinderFailureTypes extends Assert { + + private static final String MAX_DECISION_POINTS_PROPERTY = + "org.apache.ws.commons.schema.walker.maxDecisionPoints"; + + private DocumentBuilder docBuilder; + + @Before + public void setUp() throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + docBuilder = factory.newDocumentBuilder(); + } + + private void walk(String schemaBody, String xml) throws Exception { + XmlSchemaCollection collection = new XmlSchemaCollection(); + collection.read(new StringReader( + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:t=\"urn:t\"" + + " targetNamespace=\"urn:t\" elementFormDefault=\"qualified\">" + + schemaBody + "</xs:schema>")); + + XmlSchemaStateMachineGenerator generator = new XmlSchemaStateMachineGenerator(); + new XmlSchemaWalker(collection, generator) + .walk(collection.getElementByQName(new QName("urn:t", "root"))); + + XmlSchemaPathFinder<Object, Object> pathFinder = + new XmlSchemaPathFinder<Object, Object>(generator.getStartNode()); + new SaxWalkerOverDom(pathFinder) + .walk(docBuilder.parse(new InputSource(new StringReader(xml)))); + } + + private static Throwable rootCause(Throwable t) { + Throwable cause = t; + while (cause.getCause() != null && cause.getCause() != cause) { + cause = cause.getCause(); + } + return cause; + } + + @Test + public void testFacetViolationIsReportedAsXmlSchemaException() throws Exception { + try { + walk("<xs:simpleType name=\"s\"><xs:restriction base=\"xs:string\">" + + "<xs:maxLength value=\"2\"/></xs:restriction></xs:simpleType>" + + "<xs:element name=\"root\" type=\"t:s\"/>", + "<root xmlns=\"urn:t\">toolong</root>"); + fail("expected the facet violation to be reported"); + } catch (XmlSchemaException expected) { + assertTrue("the ValidationException must be kept as the cause", + expected.getCause() instanceof ValidationException); + } + } + + @Test + public void testMalformedFacetIsReportedAsXmlSchemaException() throws Exception { + try { + walk("<xs:simpleType name=\"s\"><xs:restriction base=\"xs:string\">" + + "<xs:maxLength value=\"bogus\"/></xs:restriction></xs:simpleType>" + + "<xs:element name=\"root\" type=\"t:s\"/>", + "<root xmlns=\"urn:t\">hello</root>"); + fail("expected the malformed facet to be reported"); + } catch (XmlSchemaException expected) { + assertTrue("the NumberFormatException must survive in the cause chain", + rootCause(expected) instanceof NumberFormatException); + } + } + + @Test + public void testDecisionPointBudgetSurvivesUnwrapped() throws Exception { + System.setProperty(MAX_DECISION_POINTS_PROPERTY, "1"); + try { + StringBuilder schema = new StringBuilder( + "<xs:element name=\"root\"><xs:complexType><xs:sequence>"); + StringBuilder xml = new StringBuilder("<root xmlns=\"urn:t\">"); + for (int i = 0; i < 12; i++) { + schema.append("<xs:choice minOccurs=\"0\">") + .append("<xs:element name=\"a\" type=\"xs:string\"/>") + .append("<xs:sequence><xs:element name=\"a\" type=\"xs:string\"/>") + .append("<xs:element name=\"b\" type=\"xs:string\" minOccurs=\"0\"/>") + .append("</xs:sequence></xs:choice>"); + xml.append("<a>x</a>"); + } + schema.append("<xs:element name=\"end\" type=\"xs:string\"/>") + .append("</xs:sequence></xs:complexType></xs:element>"); + xml.append("</root>"); + + walk(schema.toString(), xml.toString()); + fail("expected the decision point budget to be reported"); + } catch (XmlSchemaException expected) { + assertTrue(expected.getMessage(), + expected.getMessage().contains("decision points")); + assertNull("the budget exception must not be wrapped", expected.getCause()); + } finally { + System.clearProperty(MAX_DECISION_POINTS_PROPERTY); + } + } + + @Test + public void testValidDocumentStillWalks() throws Exception { + walk("<xs:simpleType name=\"s\"><xs:restriction base=\"xs:string\">" + + "<xs:maxLength value=\"16\"/></xs:restriction></xs:simpleType>" + + "<xs:element name=\"root\" type=\"t:s\"/>", + "<root xmlns=\"urn:t\">short</root>"); + } +}
