diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/path/MaterializedPath.java b/engine/src/main/java/org/hibernate/validator/internal/engine/path/MaterializedPath.java index 3f53656e5..9f52290fe 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/path/MaterializedPath.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/path/MaterializedPath.java @@ -6,15 +6,20 @@ import java.io.Serial; import java.io.Serializable; +import java.lang.invoke.MethodHandles; import java.util.Iterator; -import org.hibernate.validator.path.Path; +import org.hibernate.validator.internal.util.logging.Log; +import org.hibernate.validator.internal.util.logging.LoggerFactory; +import org.hibernate.validator.path.RandomAccessPath; -final class MaterializedPath implements Path, Serializable { +final class MaterializedPath implements RandomAccessPath, Serializable { + + private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() ); @Serial - private static final long serialVersionUID = 1264131890253015968L; + private static final long serialVersionUID = -329465327521818082L; private static final String PROPERTY_PATH_SEPARATOR = "."; @@ -26,6 +31,29 @@ final class MaterializedPath implements Path, Serializable { this.leafNode = nodes[nodes.length - 1]; } + @Override + public Node getLeafNode() { + return leafNode; + } + + @Override + public Node getRootNode() { + return nodes[0]; + } + + @Override + public Node getNode(int index) { + if ( index < 0 || index >= nodes.length ) { + throw LOG.pathIndexOutOfBounds( index, nodes.length ); + } + return nodes[index]; + } + + @Override + public int length() { + return nodes.length; + } + @Override public Iterator iterator() { return new MaterializedNode.NodeIterator( nodes ); @@ -72,9 +100,4 @@ static String asString(MaterializedNode currentLeafNode) { } return builder.toString(); } - - @Override - public Node getLeafNode() { - return leafNode; - } } diff --git a/engine/src/main/java/org/hibernate/validator/internal/util/logging/Log.java b/engine/src/main/java/org/hibernate/validator/internal/util/logging/Log.java index 141300a76..9c03e29d9 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/util/logging/Log.java +++ b/engine/src/main/java/org/hibernate/validator/internal/util/logging/Log.java @@ -959,4 +959,7 @@ ConstraintDefinitionException getConstraintValidatorDefinitionConstraintMismatch @Message(id = 272, value = "Using `@Valid` on a container is deprecated. You should apply the annotation on the type argument(s). (%1$s) can potentially be a container at runtime. Affected element: %2$s") void potentiallyDeprecatedUseOfValidOnContainer(@FormatWith(ClassObjectFormatter.class) Class valueType, Object context); + + @Message(id = 273, value = "Index %1$d is out of bounds for the path of length %2$d.") + IndexOutOfBoundsException pathIndexOutOfBounds(int index, int length); } diff --git a/engine/src/main/java/org/hibernate/validator/path/Path.java b/engine/src/main/java/org/hibernate/validator/path/Path.java index 744aee819..55bf8cfcb 100644 --- a/engine/src/main/java/org/hibernate/validator/path/Path.java +++ b/engine/src/main/java/org/hibernate/validator/path/Path.java @@ -8,6 +8,8 @@ /** * An extended representation of the validation path, provides Hibernate Validator specific functionality. + * + * @since 9.1 */ @Incubating public interface Path extends jakarta.validation.Path { diff --git a/engine/src/main/java/org/hibernate/validator/path/RandomAccessPath.java b/engine/src/main/java/org/hibernate/validator/path/RandomAccessPath.java new file mode 100644 index 000000000..702357f5b --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/path/RandomAccessPath.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.path; + +import java.util.RandomAccess; + +import org.hibernate.validator.Incubating; + +/** + * An extended representation of the validation path, provides Hibernate Validator specific functionality. + * Represents a path with access to the nodes by their index. + * + * @since 9.1 + */ +@Incubating +public interface RandomAccessPath extends Path, RandomAccess { + + /** + * @return The first node in the path, i.e. {@code path.iterator().next()}. + */ + jakarta.validation.Path.Node getRootNode(); + + /** + * @param index The index of the node to return. + * @return The node in the path for a given index. + * @throws IndexOutOfBoundsException if the index is out of range, i.e. {@code index < 0 || index >= length() } + */ + jakarta.validation.Path.Node getNode(int index); + + /** + * @return The length of the path, i.e. the number of nodes this path contains. + */ + int length(); +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/path/RandomAccessPathTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/path/RandomAccessPathTest.java new file mode 100644 index 000000000..6bdf9cec2 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/path/RandomAccessPathTest.java @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.validation.Path; + +import org.hibernate.validator.internal.engine.path.MutablePath; +import org.hibernate.validator.path.RandomAccessPath; + +import org.assertj.core.api.Assertions; +import org.testng.annotations.Test; + +public class RandomAccessPathTest { + + @Test + public void testParsing() { + String property = "orders[3].deliveryAddress.addressline[1]"; + Path path = MutablePath.createPathFromString( property ).materialize(); + assertThat( path ).isInstanceOf( RandomAccessPath.class ); + + if ( path instanceof RandomAccessPath randomAccessPath ) { + assertThat( randomAccessPath.length() ).isEqualTo( 4 ); + + // Get the root node and assert its properties + Path.Node elem = randomAccessPath.getRootNode(); + assertThat( elem ).isNotNull(); + assertThat( elem.getName() ).isEqualTo( "orders" ); + assertThat( elem.isInIterable() ).isFalse(); + + // Get the node by index 0 and assert its properties + elem = randomAccessPath.getNode( 0 ); + assertThat( elem ).isNotNull(); + assertThat( elem.getName() ).isEqualTo( "orders" ); + assertThat( elem.isInIterable() ).isFalse(); + + // Get the node by index 1 and assert its properties + elem = randomAccessPath.getNode( 1 ); + assertThat( elem ).isNotNull(); + assertThat( elem.getName() ).isEqualTo( "deliveryAddress" ); + assertThat( elem.isInIterable() ).isTrue(); + assertThat( elem.getIndex() ).isEqualTo( 3 ); + + // Get the node by index 2 and assert its properties + elem = randomAccessPath.getNode( 2 ); + assertThat( elem ).isNotNull(); + assertThat( elem.getName() ).isEqualTo( "addressline" ); + assertThat( elem.isInIterable() ).isFalse(); + + // Get the node by index 3 and assert its properties + elem = randomAccessPath.getNode( 3 ); + assertThat( elem ).isNotNull(); + assertThat( elem.getName() ).isNull(); + assertThat( elem.isInIterable() ).isTrue(); + assertThat( elem.getIndex() ).isEqualTo( 1 ); + + assertThatThrownBy( () -> randomAccessPath.getNode( 4 ) ) + .isInstanceOf( IndexOutOfBoundsException.class ); + + assertThat( path.toString() ).isEqualTo( property ); + } + else { + Assertions.fail( "Path is not IndexedPath" ); + } + } +}