From 0e0dc18a535efdcc397c74a9e680c0002b204ef2 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 9 May 2017 23:10:18 +0200 Subject: [PATCH] BVTCK-114 Add tests for the support of group conversion for container element types --- ...rElementGroupConversionValidationTest.java | 381 +++++++++++++++++ .../groupconversion/RegisteredAddresses.java | 106 +++++ .../tck/util/ConstraintViolationAssert.java | 390 ++++++++++++++++++ 3 files changed, 877 insertions(+) create mode 100644 tests/src/main/java/org/hibernate/beanvalidation/tck/tests/validation/groupconversion/ContainerElementGroupConversionValidationTest.java create mode 100644 tests/src/main/java/org/hibernate/beanvalidation/tck/tests/validation/groupconversion/RegisteredAddresses.java create mode 100644 tests/src/main/java/org/hibernate/beanvalidation/tck/util/ConstraintViolationAssert.java diff --git a/tests/src/main/java/org/hibernate/beanvalidation/tck/tests/validation/groupconversion/ContainerElementGroupConversionValidationTest.java b/tests/src/main/java/org/hibernate/beanvalidation/tck/tests/validation/groupconversion/ContainerElementGroupConversionValidationTest.java new file mode 100644 index 00000000..fae99430 --- /dev/null +++ b/tests/src/main/java/org/hibernate/beanvalidation/tck/tests/validation/groupconversion/ContainerElementGroupConversionValidationTest.java @@ -0,0 +1,381 @@ +/** + * Bean Validation TCK + * + * License: Apache License, Version 2.0 + * See the license.txt file in the root directory or . + */ +package org.hibernate.beanvalidation.tck.tests.validation.groupconversion; + +import static org.hibernate.beanvalidation.tck.util.ConstraintViolationAssert.pathWith; +import static org.hibernate.beanvalidation.tck.util.ConstraintViolationAssert.ConstraintViolationSetAssert.assertContainsOnlyPaths; +import static org.hibernate.beanvalidation.tck.util.TestUtil.assertCorrectNumberOfViolations; +import static org.hibernate.beanvalidation.tck.util.TestUtil.assertDescriptorKinds; +import static org.hibernate.beanvalidation.tck.util.TestUtil.assertNodeNames; +import static org.hibernate.beanvalidation.tck.util.TestUtil.webArchiveBuilder; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.time.Year; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.ElementKind; +import javax.validation.Path; +import javax.validation.groups.Default; + +import org.hibernate.beanvalidation.tck.beanvalidation.Sections; +import org.hibernate.beanvalidation.tck.tests.BaseExecutableValidatorTest; +import org.hibernate.beanvalidation.tck.util.TestUtil; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.jboss.test.audit.annotations.SpecAssertion; +import org.jboss.test.audit.annotations.SpecVersion; +import org.testng.annotations.Test; + +/** + * @author Guillaume Smet + */ +@SpecVersion(spec = "beanvalidation", version = "2.0.0") +public class ContainerElementGroupConversionValidationTest extends BaseExecutableValidatorTest { + + @Deployment + public static WebArchive createTestArchive() { + return webArchiveBuilder() + .withTestClassPackage( ContainerElementGroupConversionValidationTest.class ) + .build(); + } + + @Test + // FIXME: update spec assertions + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_GROUPSEQUENCE_GROUPCONVERSION, id = "c") + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_VALIDATIONROUTINE, id = "a") + public void testGroupConversionIsAppliedOnField() { + Set> constraintViolations = getValidator().validate( TestRegisteredAddresses.withInvalidMainAddress() ); + + + assertContainsOnlyPaths( constraintViolations, + pathWith() + .property( "mainAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "street1", true, null, 1, List.class, 0 ), + pathWith() + .property( "mainAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "zipCode", true, null, 1, List.class, 0 ) + ); + } + + @Test + // FIXME: update spec assertions + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_GROUPSEQUENCE_GROUPCONVERSION, id = "c") + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_VALIDATIONROUTINE, id = "a") + public void testSeveralGroupConversionsAppliedOnField() { + RegisteredAddresses registeredAddressesWithInvalidPreferredShipmentAddress = TestRegisteredAddresses.withInvalidPreferredShipmentAddress(); + + Set> constraintViolations = getValidator().validate( + registeredAddressesWithInvalidPreferredShipmentAddress + ); + + assertContainsOnlyPaths( constraintViolations, + pathWith() + .property( "preferredShipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "street1", true, null, 1, List.class, 0 ), + pathWith() + .property( "preferredShipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "zipCode", true, null, 1, List.class, 0 ) ); + + constraintViolations = getValidator().validate( + registeredAddressesWithInvalidPreferredShipmentAddress, + Complex.class + ); + + assertContainsOnlyPaths( constraintViolations, + pathWith() + .property( "preferredShipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property("doorCode", true, null, 1, List.class, 0 ) + ); + + constraintViolations = getValidator().validate( + registeredAddressesWithInvalidPreferredShipmentAddress, + Default.class, + Complex.class + ); + + assertContainsOnlyPaths( constraintViolations, + pathWith() + .property( "preferredShipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "street1", true, null, 1, List.class, 0 ), + pathWith() + .property( "preferredShipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "zipCode", true, null, 1, List.class, 0 ), + pathWith() + .property( "preferredShipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "doorCode", true, null, 1, List.class, 0 ) + ); + + constraintViolations = getValidator().validate( + registeredAddressesWithInvalidPreferredShipmentAddress, + Complete.class + ); + + assertContainsOnlyPaths( constraintViolations, + pathWith() + .property( "preferredShipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "street1", true, null, 1, List.class, 0 ), + pathWith() + .property( "preferredShipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "zipCode", true, null, 1, List.class, 0 ), + pathWith() + .property( "preferredShipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "doorCode", true, null, 1, List.class, 0 ) + ); + } + + @Test + // FIXME: update spec assertions + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_GROUPSEQUENCE_GROUPCONVERSION, id = "c") + public void testGroupConversionIsAppliedOnProperty() { + Set> constraintViolations = getValidator().validate( TestRegisteredAddresses.withInvalidShipmentAddress() ); + + assertContainsOnlyPaths( constraintViolations, + pathWith() + .property( "shipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "street1", true, null, 1, List.class, 0 ), + pathWith() + .property( "shipmentAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "zipCode", true, null, 1, List.class, 0 ) + ); + } + + @Test + // FIXME: update spec assertions + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_GROUPSEQUENCE_GROUPCONVERSION, id = "c") + public void testGroupConversionIsAppliedOnMethodReturnValue() throws Exception { + //given + RegisteredAddresses registeredAddresses = TestRegisteredAddresses.validRegisteredAddresses(); + Method method = RegisteredAddresses.class.getMethod( "retrieveMainAddresses" ); + Object returnValue = TestAddresses.wrap( TestAddresses.withInvalidStreet1() ); + + //when + Set> constraintViolations = getExecutableValidator() + .validateReturnValue( registeredAddresses, method, returnValue ); + + //then + assertCorrectNumberOfViolations( constraintViolations, 1 ); + + Path propertyPath = constraintViolations.iterator().next().getPropertyPath(); + + assertDescriptorKinds( propertyPath, ElementKind.METHOD, ElementKind.RETURN_VALUE, ElementKind.CONTAINER_ELEMENT, ElementKind.PROPERTY ); + assertNodeNames( propertyPath, "retrieveMainAddresses", TestUtil.RETURN_VALUE_NODE_NAME, "", "street1" ); + } + + @Test + // FIXME: update spec assertions + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_GROUPSEQUENCE_GROUPCONVERSION, id = "c") + public void testGroupConversionIsAppliedOnMethodParameter() throws Exception { + //given + RegisteredAddresses registeredAddresses = TestRegisteredAddresses.validRegisteredAddresses(); + Method method = RegisteredAddresses.class.getMethod( "setMainAddress", Map.class ); + Object[] arguments = new Object[] { TestAddresses.wrap( TestAddresses.withInvalidStreet1() ) }; + + //when + Set> constraintViolations = getExecutableValidator() + .validateParameters( registeredAddresses, method, arguments ); + + //then + assertCorrectNumberOfViolations( constraintViolations, 1 ); + + Path propertyPath = constraintViolations.iterator().next().getPropertyPath(); + + assertDescriptorKinds( propertyPath, ElementKind.METHOD, ElementKind.PARAMETER, ElementKind.CONTAINER_ELEMENT, ElementKind.PROPERTY ); + assertNodeNames( propertyPath, "setMainAddress", "mainAddresses", "", "street1" ); + } + + @Test + // FIXME: update spec assertions + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_GROUPSEQUENCE_GROUPCONVERSION, id = "c") + public void testGroupConversionIsAppliedOnConstructorReturnValue() throws Exception { + //given + Constructor constructor = RegisteredAddresses.class.getConstructor( Map.class ); + RegisteredAddresses createdObject = TestRegisteredAddresses.withMainAddressInvalidStreet1(); + + //when + Set> constraintViolations = getExecutableValidator() + .validateConstructorReturnValue( constructor, createdObject ); + + //then + assertCorrectNumberOfViolations( constraintViolations, 1 ); + + Path propertyPath = constraintViolations.iterator().next().getPropertyPath(); + + assertDescriptorKinds( + propertyPath, + ElementKind.CONSTRUCTOR, + ElementKind.RETURN_VALUE, + ElementKind.PROPERTY, + ElementKind.CONTAINER_ELEMENT, + ElementKind.PROPERTY + ); + assertNodeNames( propertyPath, "RegisteredAddresses", TestUtil.RETURN_VALUE_NODE_NAME, "mainAddresses", "", "street1" ); + } + + @Test + // FIXME: update spec assertions + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_GROUPSEQUENCE_GROUPCONVERSION, id = "c") + public void testGroupConversionIsAppliedOnConstructorParameter() throws Exception { + //given + Constructor constructor = RegisteredAddresses.class.getConstructor( Map.class ); + Object[] arguments = new Object[] { TestAddresses.wrap( TestAddresses.withInvalidStreet1() ) }; + + //when + Set> constraintViolations = getExecutableValidator() + .validateConstructorParameters( constructor, arguments ); + + //then + assertCorrectNumberOfViolations( constraintViolations, 1 ); + + Path propertyPath = constraintViolations.iterator().next().getPropertyPath(); + + assertDescriptorKinds( propertyPath, ElementKind.CONSTRUCTOR, ElementKind.PARAMETER, ElementKind.CONTAINER_ELEMENT, ElementKind.PROPERTY ); + assertNodeNames( propertyPath, "RegisteredAddresses", "mainAddresses", "", "street1" ); + } + + @Test + // FIXME: update spec assertions + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_GROUPSEQUENCE_GROUPCONVERSION, id = "d") + public void testGroupConversionIsNotExecutedRecursively() { + Set> constraintViolations = getValidator().validate( TestRegisteredAddresses.withInvalidOfficeAddress() ); + + assertContainsOnlyPaths( constraintViolations, + pathWith() + .property( "officeAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "street1", true, null, 1, List.class, 0 ), + pathWith() + .property( "officeAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "zipCode", true, null, 1, List.class, 0 ) + ); + + constraintViolations = getValidator().validate( + TestRegisteredAddresses.withInvalidOfficeAddress(), + BasicPostal.class + ); + + assertContainsOnlyPaths( constraintViolations, + pathWith() + .property( "officeAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "doorCode", true, null, 1, List.class, 0 ) + ); + } + + @Test + // FIXME: update spec assertions + @SpecAssertion(section = Sections.CONSTRAINTDECLARATIONVALIDATIONPROCESS_GROUPSEQUENCE_GROUPCONVERSION, id = "g") + public void testGroupConversionWithSequenceAsTo() { + RegisteredAddresses registeredAddresses = TestRegisteredAddresses.validRegisteredAddresses(); + + Set> constraintViolations = getValidator().validate( registeredAddresses ); + assertCorrectNumberOfViolations( constraintViolations, 0 ); + + registeredAddresses.getWeekendAddresses().get( TestRegisteredAddresses.REFERENCE_YEAR ).get( 0 ).setDoorCode( "ABC" ); + constraintViolations = getValidator().validate( registeredAddresses ); + assertContainsOnlyPaths( constraintViolations, + pathWith() + .property( "weekendAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "doorCode", true, null, 0, List.class, 0 ) + ); + + registeredAddresses.getWeekendAddresses().get( TestRegisteredAddresses.REFERENCE_YEAR ).get( 0 ).setStreet1( null ); + constraintViolations = getValidator().validate( registeredAddresses ); + assertContainsOnlyPaths( constraintViolations, + pathWith() + .property( "weekendAddresses" ) + .containerElement( "", true, TestRegisteredAddresses.REFERENCE_YEAR, null, Map.class, 1 ) + .property( "street1", true, null, 0, List.class, 0 ) ); + } + + private static class TestRegisteredAddresses { + + public final static Year REFERENCE_YEAR = Year.of(2016); + + public static RegisteredAddresses validRegisteredAddresses() { + return new RegisteredAddresses() + .addMainAddress( REFERENCE_YEAR, TestAddresses.validAddress() ) + .addShipmentAddress( REFERENCE_YEAR, TestAddresses.validAddress() ) + .addPreferredShipmentAddress( REFERENCE_YEAR, TestAddresses.validAddress() ) + .addOfficeAddress( REFERENCE_YEAR, TestAddresses.validAddress() ) + .addWeekendAddress( REFERENCE_YEAR, TestAddresses.validAddress() ); + } + + public static RegisteredAddresses withInvalidMainAddress() { + RegisteredAddresses registeredAddresses = validRegisteredAddresses(); + registeredAddresses.addMainAddress( REFERENCE_YEAR, TestAddresses.invalidAddress() ); + return registeredAddresses; + } + + public static RegisteredAddresses withMainAddressInvalidStreet1() { + RegisteredAddresses registeredAddresses = validRegisteredAddresses(); + registeredAddresses.addMainAddress( REFERENCE_YEAR, TestAddresses.withInvalidStreet1() ); + return registeredAddresses; + } + + public static RegisteredAddresses withInvalidShipmentAddress() { + RegisteredAddresses registeredAddresses = validRegisteredAddresses(); + registeredAddresses.addShipmentAddress( REFERENCE_YEAR, TestAddresses.invalidAddress() ); + return registeredAddresses; + } + + public static RegisteredAddresses withInvalidPreferredShipmentAddress() { + RegisteredAddresses registeredAddresses = validRegisteredAddresses(); + registeredAddresses.addPreferredShipmentAddress( REFERENCE_YEAR, TestAddresses.invalidAddress() ); + return registeredAddresses; + } + + public static RegisteredAddresses withInvalidOfficeAddress() { + RegisteredAddresses registeredAddresses = validRegisteredAddresses(); + registeredAddresses.addOfficeAddress( REFERENCE_YEAR, TestAddresses.invalidAddress() ); + return registeredAddresses; + } + } + + private static class TestAddresses { + + public static Address validAddress() { + return new Address( "Main Street", "c/o Hitchcock", "123", "AB" ); + } + + public static Address invalidAddress() { + return new Address( null, null, "12", "ABC" ); + } + + public static Address withInvalidStreet1() { + Address address = validAddress(); + address.setStreet1( null ); + return address; + } + + public static Map> wrap(Address address) { + Map> addresses = new HashMap<>(); + addresses.put( TestRegisteredAddresses.REFERENCE_YEAR, Arrays.asList( address ) ); + return addresses; + } + } +} diff --git a/tests/src/main/java/org/hibernate/beanvalidation/tck/tests/validation/groupconversion/RegisteredAddresses.java b/tests/src/main/java/org/hibernate/beanvalidation/tck/tests/validation/groupconversion/RegisteredAddresses.java new file mode 100644 index 00000000..744c5b3c --- /dev/null +++ b/tests/src/main/java/org/hibernate/beanvalidation/tck/tests/validation/groupconversion/RegisteredAddresses.java @@ -0,0 +1,106 @@ +/** + * Bean Validation TCK + * + * License: Apache License, Version 2.0 + * See the license.txt file in the root directory or . + */ +package org.hibernate.beanvalidation.tck.tests.validation.groupconversion; + +import java.time.Year; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.validation.Valid; +import javax.validation.groups.ConvertGroup; +import javax.validation.groups.Default; + +/** + * @author Guillaume Smet + */ +public class RegisteredAddresses { + + private final Map> mainAddresses = new HashMap<>(); + + private final Map> shipmentAddresses = new HashMap<>(); + + private final Map> preferredShipmentAddresses = new HashMap<>(); + + private final Map> officeAddresses = new HashMap<>(); + + private final Map> weekendAddresses = new HashMap<>(); + + public RegisteredAddresses() { + } + + @Valid + @ConvertGroup(from = Default.class, to = BasicPostal.class) + public RegisteredAddresses(Map> mainAddresses) { + this.mainAddresses.putAll( mainAddresses ); + } + + public RegisteredAddresses addMainAddress(Year year, Address address) { + mainAddresses.computeIfAbsent( year, y -> new ArrayList<>() ).add( address ); + return this; + } + + public RegisteredAddresses addShipmentAddress(Year year, Address address) { + shipmentAddresses.computeIfAbsent( year, y -> new ArrayList<>() ).add( address ); + return this; + } + + public RegisteredAddresses addPreferredShipmentAddress(Year year, Address address) { + preferredShipmentAddresses.computeIfAbsent( year, y -> new ArrayList<>() ).add( address ); + return this; + } + + public RegisteredAddresses addOfficeAddress(Year year, Address address) { + officeAddresses.computeIfAbsent( year, y -> new ArrayList<>() ).add( address ); + return this; + } + + public RegisteredAddresses addWeekendAddress(Year year, Address address) { + weekendAddresses.computeIfAbsent( year, y -> new ArrayList<>() ).add( address ); + return this; + } + + public Map> getMainAddresses() { + return mainAddresses; + } + + public void setMainAddress(Map> mainAddresses) { + this.mainAddresses.clear(); + this.mainAddresses.putAll( mainAddresses ); + } + + public Map> getPreferredShipmentAddresses() { + return preferredShipmentAddresses; + } + + public Map> getShipmentAddresses() { + return shipmentAddresses; + } + + public Map> getOfficeAddresses() { + return officeAddresses; + } + + public Map> getWeekendAddresses() { + return weekendAddresses; + } + + public Map> retrieveMainAddresses() { + return mainAddresses; + } + + public Map> retrieveWeekendAddresses() { + return weekendAddresses; + } +} diff --git a/tests/src/main/java/org/hibernate/beanvalidation/tck/util/ConstraintViolationAssert.java b/tests/src/main/java/org/hibernate/beanvalidation/tck/util/ConstraintViolationAssert.java new file mode 100644 index 00000000..af38b91d --- /dev/null +++ b/tests/src/main/java/org/hibernate/beanvalidation/tck/util/ConstraintViolationAssert.java @@ -0,0 +1,390 @@ +/** + * Bean Validation TCK + * + * License: Apache License, Version 2.0 + * See the license.txt file in the root directory or . + */ +package org.hibernate.beanvalidation.tck.util; + +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.ElementKind; +import javax.validation.Path; + +/** + * This class provides useful functions to assert correctness of constraint violations raised + * during tests. + * + * @author Kevin Pollet <kevin.pollet@serli.com> (C) 2011 SERLI + * @author Hardy Ferentschik + * @author Gunnar Morling + */ +public final class ConstraintViolationAssert { + + /** + * Expected name for cross-parameter nodes. + */ + private static final String CROSS_PARAMETER_NODE_NAME = ""; + + /** + * Expected name for cross-parameter nodes. + */ + private static final String RETURN_VALUE_NODE_NAME = ""; + + /** + * Private constructor in order to avoid instantiation. + */ + private ConstraintViolationAssert() { + } + + public static PathExpectation pathWith() { + return new PathExpectation(); + } + + public static class ConstraintViolationSetAssert { + + public static void assertContainsOnlyPaths(Set> actualViolations, PathExpectation... paths) { + assertNotNull( paths ); + + List actualPaths = new ArrayList<>(); + + for ( ConstraintViolation violation : actualViolations ) { + actualPaths.add( new PathExpectation( violation.getPropertyPath() ) ); + } + + assertContainsOnly( actualPaths, paths ); + } + + public static void assertContainsPath(Set> actualViolations, PathExpectation expectedPath) { + assertNotNull( expectedPath ); + + List actualPaths = new ArrayList<>(); + for ( ConstraintViolation violation : actualViolations ) { + PathExpectation actual = new PathExpectation( violation.getPropertyPath() ); + if ( actual.equals( expectedPath ) ) { + return; + } + actualPaths.add( actual ); + } + + fail( String.format( "Didn't find path <%s> in actual paths <%s>.", expectedPath, actualPaths ) ); + } + + public static void assertContainsPaths(Set> actualViolations, PathExpectation... expectedPaths) { + for ( PathExpectation pathExpectation : expectedPaths ) { + assertContainsPath( actualViolations, pathExpectation ); + } + } + } + + private static void assertContainsOnly(Collection actual, Object[] expected) { + assertNotNull( actual ); + assertNotNull( expected ); + + if ( expected.length == actual.size() ) { + boolean equal = true; + for ( Object expectedElement : expected ) { + if ( !actual.contains( expectedElement ) ) { + equal = false; + } + } + if ( equal ) { + return; + } + } + + fail( String.format( "Should only contain the expected element: <%s>. Actual: <%s>.", Arrays.toString( expected ), actual ) ); + } + + /** + * A property path expected to be returned by a given {@link ConstraintViolation}. + */ + public static class PathExpectation { + + private final List nodes = new ArrayList<>(); + + private PathExpectation() { + } + + private PathExpectation(Path propertyPath) { + for ( Path.Node node : propertyPath ) { + Integer parameterIndex = null; + if ( node.getKind() == ElementKind.PARAMETER ) { + parameterIndex = node.as( Path.ParameterNode.class ).getParameterIndex(); + } + Class containerClass = getContainerClass( node ); + Integer typeArgumentIndex = getTypeArgumentIndex( node ); + nodes.add( + new NodeExpectation( + node.getName(), + node.getKind(), + node.isInIterable(), + node.getKey(), + node.getIndex(), + parameterIndex, + containerClass, + typeArgumentIndex + ) + ); + } + } + + public PathExpectation property(String name) { + nodes.add( new NodeExpectation( name, ElementKind.PROPERTY ) ); + return this; + } + + public PathExpectation property(String name, Class containerClass, Integer typeArgumentIndex) { + nodes.add( new NodeExpectation( name, ElementKind.PROPERTY, false, null, null, null, containerClass, typeArgumentIndex ) ); + return this; + } + + public PathExpectation property(String name, boolean inIterable, Object key, Integer index) { + nodes.add( new NodeExpectation( name, ElementKind.PROPERTY, inIterable, key, index, null, null, null ) ); + return this; + } + + public PathExpectation property(String name, boolean inIterable, Object key, Integer index, Class containerClass, Integer typeArgumentIndex) { + nodes.add( new NodeExpectation( name, ElementKind.PROPERTY, inIterable, key, index, null, containerClass, typeArgumentIndex ) ); + return this; + } + + public PathExpectation bean() { + nodes.add( new NodeExpectation( null, ElementKind.BEAN ) ); + return this; + } + + public PathExpectation bean(boolean inIterable, Object key, Integer index) { + nodes.add( new NodeExpectation( null, ElementKind.BEAN, inIterable, key, index, null, null, null ) ); + return this; + } + + public PathExpectation bean(boolean inIterable, Object key, Integer index, Class containerClass, Integer typeArgumentIndex) { + nodes.add( new NodeExpectation( null, ElementKind.BEAN, inIterable, key, index, null, containerClass, typeArgumentIndex ) ); + return this; + } + + public PathExpectation method(String name) { + nodes.add( new NodeExpectation( name, ElementKind.METHOD ) ); + return this; + } + + public PathExpectation parameter(String name, int index) { + nodes.add( new NodeExpectation( name, ElementKind.PARAMETER, false, null, null, index, null, null ) ); + return this; + } + + public PathExpectation crossParameter() { + nodes.add( new NodeExpectation( CROSS_PARAMETER_NODE_NAME, ElementKind.CROSS_PARAMETER ) ); + return this; + } + + public PathExpectation returnValue() { + nodes.add( new NodeExpectation( RETURN_VALUE_NODE_NAME, ElementKind.RETURN_VALUE ) ); + return this; + } + + public PathExpectation containerElement(String name, boolean inIterable, Object key, Integer index, Class containerClass, Integer typeArgumentIndex) { + nodes.add( new NodeExpectation( name, ElementKind.CONTAINER_ELEMENT, inIterable, key, index, null, containerClass, typeArgumentIndex ) ); + return this; + } + + @Override + public String toString() { + String lineBreak = System.getProperty( "line.separator" ); + StringBuilder asString = new StringBuilder( lineBreak + "PathExpectation(" + lineBreak ); + for ( NodeExpectation node : nodes ) { + asString.append( " " ).append( node ).append( lineBreak ); + } + + return asString.append( ")" ).toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ( ( nodes == null ) ? 0 : nodes.hashCode() ); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) { + return true; + } + if ( obj == null ) { + return false; + } + if ( getClass() != obj.getClass() ) { + return false; + } + PathExpectation other = (PathExpectation) obj; + if ( nodes == null ) { + if ( other.nodes != null ) { + return false; + } + } + else if ( !nodes.equals( other.nodes ) ) { + return false; + } + return true; + } + } + + /** + * A node expected to be contained in the property path returned by a given {@link ConstraintViolation}. + */ + private static class NodeExpectation { + private final String name; + private final ElementKind kind; + private final boolean inIterable; + private final Object key; + private final Integer index; + private final Integer parameterIndex; + private final Class containerClass; + private final Integer typeArgumentIndex; + + private NodeExpectation(String name, ElementKind kind) { + this( name, kind, false, null, null, null, null, null ); + } + + private NodeExpectation(String name, ElementKind kind, boolean inIterable, Object key, Integer index, + Integer parameterIndex, Class containerClass, Integer typeArgumentIndex) { + this.name = name; + this.kind = kind; + this.inIterable = inIterable; + this.key = key; + this.index = index; + this.parameterIndex = parameterIndex; + this.containerClass = containerClass; + this.typeArgumentIndex = typeArgumentIndex; + } + + @Override + public String toString() { + return "NodeExpectation(" + name + ", " + kind + ", " + inIterable + + ", " + key + ", " + index + ", " + parameterIndex + ", " + containerClass + ", " + typeArgumentIndex + ")"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ( inIterable ? 1231 : 1237 ); + result = prime * result + ( ( index == null ) ? 0 : index.hashCode() ); + result = prime * result + ( ( key == null ) ? 0 : key.hashCode() ); + result = prime * result + ( ( kind == null ) ? 0 : kind.hashCode() ); + result = prime * result + ( ( name == null ) ? 0 : name.hashCode() ); + result = prime * result + ( ( parameterIndex == null ) ? 0 : parameterIndex.hashCode() ); + result = prime * result + ( ( containerClass == null ) ? 0 : containerClass.hashCode() ); + result = prime * result + ( ( typeArgumentIndex == null ) ? 0 : typeArgumentIndex.hashCode() ); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) { + return true; + } + if ( obj == null ) { + return false; + } + if ( getClass() != obj.getClass() ) { + return false; + } + NodeExpectation other = (NodeExpectation) obj; + if ( inIterable != other.inIterable ) { + return false; + } + if ( index == null ) { + if ( other.index != null ) { + return false; + } + } + else if ( !index.equals( other.index ) ) { + return false; + } + if ( key == null ) { + if ( other.key != null ) { + return false; + } + } + else if ( !key.equals( other.key ) ) { + return false; + } + if ( kind != other.kind ) { + return false; + } + if ( name == null ) { + if ( other.name != null ) { + return false; + } + } + else if ( !name.equals( other.name ) ) { + return false; + } + if ( parameterIndex == null ) { + if ( other.parameterIndex != null ) { + return false; + } + } + else if ( !parameterIndex.equals( other.parameterIndex ) ) { + return false; + } + if ( containerClass == null ) { + if ( other.containerClass != null ) { + return false; + } + } + else if ( !containerClass.equals( other.containerClass ) ) { + return false; + } + if ( typeArgumentIndex == null ) { + if ( other.typeArgumentIndex != null ) { + return false; + } + } + else if ( !typeArgumentIndex.equals( other.typeArgumentIndex ) ) { + return false; + } + return true; + } + } + + private static Class getContainerClass(Path.Node node) { + Class containerClass = null; + if ( node.getKind() == ElementKind.PROPERTY ) { + containerClass = node.as( Path.PropertyNode.class ).getContainerClass(); + } + if ( node.getKind() == ElementKind.BEAN ) { + containerClass = node.as( Path.BeanNode.class ).getContainerClass(); + } + if ( node.getKind() == ElementKind.CONTAINER_ELEMENT ) { + containerClass = node.as( Path.ContainerElementNode.class ).getContainerClass(); + } + return containerClass; + } + + private static Integer getTypeArgumentIndex(Path.Node node) { + Integer typeArgumentIndex = null; + if ( node.getKind() == ElementKind.PROPERTY ) { + typeArgumentIndex = node.as( Path.PropertyNode.class ).getTypeArgumentIndex(); + } + if ( node.getKind() == ElementKind.BEAN ) { + typeArgumentIndex = node.as( Path.BeanNode.class ).getTypeArgumentIndex(); + } + if ( node.getKind() == ElementKind.CONTAINER_ELEMENT ) { + typeArgumentIndex = node.as( Path.ContainerElementNode.class ).getTypeArgumentIndex(); + } + return typeArgumentIndex; + } +}