diff --git a/engine/src/main/java/org/hibernate/validator/BaseHibernateValidatorConfiguration.java b/engine/src/main/java/org/hibernate/validator/BaseHibernateValidatorConfiguration.java index 93eb1900f6..dc0de55251 100644 --- a/engine/src/main/java/org/hibernate/validator/BaseHibernateValidatorConfiguration.java +++ b/engine/src/main/java/org/hibernate/validator/BaseHibernateValidatorConfiguration.java @@ -31,6 +31,7 @@ import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator; import org.hibernate.validator.spi.scripting.ScriptEvaluator; import org.hibernate.validator.spi.scripting.ScriptEvaluatorFactory; +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter; /** * Base interface for Hibernate Validator specific configurations. @@ -486,4 +487,7 @@ default S locales(Locale... locales) { */ @Incubating S failFastOnPropertyViolation(boolean failFastOnPropertyViolation); + + @Incubating + S processedBeansTrackingVoter(ProcessedBeansTrackingVoter processedBeanTrackingVoter); } diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/AbstractConfigurationImpl.java b/engine/src/main/java/org/hibernate/validator/internal/engine/AbstractConfigurationImpl.java index 61a9436457..768ae1dafc 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/AbstractConfigurationImpl.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/AbstractConfigurationImpl.java @@ -62,6 +62,7 @@ import org.hibernate.validator.spi.properties.GetterPropertySelectionStrategy; import org.hibernate.validator.spi.resourceloading.ResourceBundleLocator; import org.hibernate.validator.spi.scripting.ScriptEvaluatorFactory; +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter; /** * Hibernate specific {@code Configuration} implementation. @@ -129,6 +130,7 @@ public abstract class AbstractConfigurationImpl getProgrammaticMappings() { return programmaticMappings; } diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/PredefinedScopeValidatorFactoryImpl.java b/engine/src/main/java/org/hibernate/validator/internal/engine/PredefinedScopeValidatorFactoryImpl.java index 1fc874b195..eb5a55bd31 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/PredefinedScopeValidatorFactoryImpl.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/PredefinedScopeValidatorFactoryImpl.java @@ -46,8 +46,10 @@ import org.hibernate.validator.PredefinedScopeHibernateValidatorFactory; import org.hibernate.validator.internal.cfg.context.DefaultConstraintMapping; import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager; +import org.hibernate.validator.internal.engine.constraintvalidation.HibernateConstraintValidatorInitializationContextImpl; import org.hibernate.validator.internal.engine.constraintvalidation.PredefinedScopeConstraintValidatorManagerImpl; import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator; +import org.hibernate.validator.internal.engine.tracking.DefaultProcessedBeansTrackingVoter; import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorManager; import org.hibernate.validator.internal.metadata.PredefinedScopeBeanMetaDataManager; import org.hibernate.validator.internal.metadata.core.ConstraintHelper; @@ -118,6 +120,14 @@ public PredefinedScopeValidatorFactoryImpl(ConfigurationState configurationState determineAllowParallelMethodsDefineParameterConstraints( hibernateSpecificConfig, properties ) ).build(); + ExecutableParameterNameProvider parameterNameProvider = new ExecutableParameterNameProvider( configurationState.getParameterNameProvider() ); + ScriptEvaluatorFactory scriptEvaluatorFactory = determineScriptEvaluatorFactory( configurationState, properties, externalClassLoader ); + Duration temporalValidationTolerance = determineTemporalValidationTolerance( configurationState, properties ); + + HibernateConstraintValidatorInitializationContextImpl constraintValidatorInitializationContext = new HibernateConstraintValidatorInitializationContextImpl( + scriptEvaluatorFactory, configurationState.getClockProvider(), temporalValidationTolerance ); + + this.validatorFactoryScopedContext = new ValidatorFactoryScopedContext( configurationState.getMessageInterpolator(), configurationState.getTraversableResolver(), @@ -128,15 +138,16 @@ public PredefinedScopeValidatorFactoryImpl(ConfigurationState configurationState determineFailFast( hibernateSpecificConfig, properties ), determineFailFastOnPropertyViolation( hibernateSpecificConfig, properties ), determineTraversableResolverResultCacheEnabled( hibernateSpecificConfig, properties ), + determineShowValidatedValuesInTraceLogs( hibernateSpecificConfig, properties ), determineConstraintValidatorPayload( hibernateSpecificConfig ), determineConstraintExpressionLanguageFeatureLevel( hibernateSpecificConfig, properties ), determineCustomViolationExpressionLanguageFeatureLevel( hibernateSpecificConfig, properties ), - determineShowValidatedValuesInTraceLogs( hibernateSpecificConfig, properties ) + constraintValidatorInitializationContext ); this.constraintValidatorManager = new PredefinedScopeConstraintValidatorManagerImpl( configurationState.getConstraintValidatorFactory(), - this.validatorFactoryScopedContext.getConstraintValidatorInitializationContext() + constraintValidatorInitializationContext ); this.validationOrderGenerator = new ValidationOrderGenerator(); @@ -147,11 +158,14 @@ public PredefinedScopeValidatorFactoryImpl(ConfigurationState configurationState this.valueExtractorManager = new ValueExtractorManager( configurationState.getValueExtractors() ); ConstraintHelper constraintHelper = ConstraintHelper.forBuiltinConstraints( hibernateSpecificConfig.getBuiltinConstraints(), - hibernateSpecificConfig.isIncludeBeansAndConstraintsDefinedOnlyInXml() ); + hibernateSpecificConfig.isIncludeBeansAndConstraintsDefinedOnlyInXml() + ); TypeResolutionHelper typeResolutionHelper = new TypeResolutionHelper(); - ConstraintCreationContext constraintCreationContext = new ConstraintCreationContext( constraintHelper, - constraintValidatorManager, typeResolutionHelper, valueExtractorManager ); + ConstraintCreationContext constraintCreationContext = new ConstraintCreationContext( + constraintHelper, + constraintValidatorManager, typeResolutionHelper, valueExtractorManager + ); ExecutableHelper executableHelper = new ExecutableHelper( typeResolutionHelper ); JavaBeanHelper javaBeanHelper = new JavaBeanHelper( getterPropertySelectionStrategy, propertyNodeNameProvider ); @@ -164,15 +178,18 @@ public PredefinedScopeValidatorFactoryImpl(ConfigurationState configurationState javaBeanHelper, externalClassLoader ), - constraintHelper ); + constraintHelper + ); // we parse all XML mappings but only register constraint validators and delay constraint mappings building till // we collect all the constraint validators. // HV-302; don't load XmlMappingParser if not necessary MappingXmlParser mappingParser = null; if ( !configurationState.getMappingStreams().isEmpty() ) { - mappingParser = new MappingXmlParser( constraintCreationContext, - javaBeanHelper, externalClassLoader ); + mappingParser = new MappingXmlParser( + constraintCreationContext, + javaBeanHelper, externalClassLoader + ); mappingParser.parse( configurationState.getMappingStreams() ); } @@ -202,15 +219,32 @@ public PredefinedScopeValidatorFactoryImpl(ConfigurationState configurationState xmlMetaDataProvider = null; } + // collect all metadata, I don't think we need this work to be in BeanMetaDataManager contract, it can be a specific class (or private method if simple enough) + // it's basically the content of PredefinedScopeBeanMetaDataManager constructor + // the metadata wouldn't be complete because we want to inject the tracking information + + // then you build the tracking information from these incomplete metadata + + // finally you create a PredefinedScopeBeanMetaDataManager with the augmented metadata pushed to it + // you will need to augment both BeanMetaData and ExecutableMetaData + // I would prototype BeanMetaData first then discuss it before going further + + // Note: we want classes to be immutable + // Might be a good idea to push a default method to BeanMetaData as enabling tracking is the default behavior we want + // Maybe first try composition and benchmark it and if good enough, we keep it + this.beanMetaDataManager = new PredefinedScopeBeanMetaDataManager( constraintCreationContext, executableHelper, - validatorFactoryScopedContext.getParameterNameProvider(), + parameterNameProvider, javaBeanHelper, validationOrderGenerator, buildMetaDataProviders( constraintCreationContext, xmlMetaDataProvider, constraintMappings ), methodValidationConfiguration, determineBeanMetaDataClassNormalizer( hibernateSpecificConfig ), + ( hibernateSpecificConfig.getProcessedBeansTrackingVoter() != null ) + ? hibernateSpecificConfig.getProcessedBeansTrackingVoter() + : new DefaultProcessedBeansTrackingVoter(), beanClassesToInitialize ); @@ -281,6 +315,10 @@ public boolean isTraversableResolverResultCacheEnabled() { return validatorFactoryScopedContext.isTraversableResolverResultCacheEnabled(); } + public PredefinedScopeBeanMetaDataManager getBeanMetaDataManager() { + return beanMetaDataManager; + } + @Override public T unwrap(Class type) { // allow unwrapping into public super types @@ -322,7 +360,8 @@ Validator createValidator(ValidatorFactoryScopedContext validatorFactoryScopedCo private static List buildMetaDataProviders( ConstraintCreationContext constraintCreationContext, XmlMetaDataProvider xmlMetaDataProvider, - Set constraintMappings) { + Set constraintMappings + ) { List metaDataProviders = newArrayList(); if ( xmlMetaDataProvider != null ) { metaDataProviders.add( xmlMetaDataProvider ); diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryImpl.java b/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryImpl.java index b9b5a98419..aa0f7a0187 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryImpl.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryImpl.java @@ -48,6 +48,7 @@ import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager; import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManagerImpl; import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator; +import org.hibernate.validator.internal.engine.tracking.DefaultProcessedBeansTrackingVoter; import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorManager; import org.hibernate.validator.internal.metadata.BeanMetaDataManager; import org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl; @@ -68,6 +69,7 @@ import org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider; import org.hibernate.validator.spi.properties.GetterPropertySelectionStrategy; import org.hibernate.validator.spi.scripting.ScriptEvaluatorFactory; +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter; /** * Factory returning initialized {@code Validator} instances. This is the Hibernate Validator default @@ -133,6 +135,8 @@ public class ValidatorFactoryImpl implements HibernateValidatorFactory { private final ValidationOrderGenerator validationOrderGenerator; + private final ProcessedBeansTrackingVoter processedBeansTrackingVoter; + public ValidatorFactoryImpl(ConfigurationState configurationState) { ClassLoader externalClassLoader = determineExternalClassLoader( configurationState ); @@ -162,10 +166,10 @@ public ValidatorFactoryImpl(ConfigurationState configurationState) { determineFailFast( hibernateSpecificConfig, properties ), determineFailFastOnPropertyViolation( hibernateSpecificConfig, properties ), determineTraversableResolverResultCacheEnabled( hibernateSpecificConfig, properties ), + determineShowValidatedValuesInTraceLogs( hibernateSpecificConfig, properties ), determineConstraintValidatorPayload( hibernateSpecificConfig ), determineConstraintExpressionLanguageFeatureLevel( hibernateSpecificConfig, properties ), - determineCustomViolationExpressionLanguageFeatureLevel( hibernateSpecificConfig, properties ), - determineShowValidatedValuesInTraceLogs( hibernateSpecificConfig, properties ) + determineCustomViolationExpressionLanguageFeatureLevel( hibernateSpecificConfig, properties ) ); ConstraintValidatorManager constraintValidatorManager = new ConstraintValidatorManagerImpl( @@ -226,6 +230,10 @@ public ValidatorFactoryImpl(ConfigurationState configurationState) { this.xmlMetaDataProvider = null; } + this.processedBeansTrackingVoter = ( hibernateSpecificConfig != null && hibernateSpecificConfig.getProcessedBeansTrackingVoter() != null ) + ? hibernateSpecificConfig.getProcessedBeansTrackingVoter() + : new DefaultProcessedBeansTrackingVoter(); + if ( LOG.isDebugEnabled() ) { logValidatorFactoryScopedConfiguration( validatorFactoryScopedContext ); } @@ -349,7 +357,8 @@ Validator createValidator(ConstraintValidatorFactory constraintValidatorFactory, beanMetadataClassNormalizer, validationOrderGenerator, buildMetaDataProviders(), - methodValidationConfiguration + methodValidationConfiguration, + processedBeansTrackingVoter ) ); diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryScopedContext.java b/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryScopedContext.java index 558b4a860b..ae56d924da 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryScopedContext.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/ValidatorFactoryScopedContext.java @@ -101,10 +101,10 @@ public class ValidatorFactoryScopedContext { boolean failFast, boolean failFastOnPropertyViolation, boolean traversableResolverResultCacheEnabled, + boolean showValidatedValuesInTraceLogs, Object constraintValidatorPayload, ExpressionLanguageFeatureLevel constraintExpressionLanguageFeatureLevel, - ExpressionLanguageFeatureLevel customViolationExpressionLanguageFeatureLevel, - boolean showValidatedValuesInTraceLogs) { + ExpressionLanguageFeatureLevel customViolationExpressionLanguageFeatureLevel) { this( messageInterpolator, traversableResolver, parameterNameProvider, clockProvider, temporalValidationTolerance, scriptEvaluatorFactory, failFast, failFastOnPropertyViolation, traversableResolverResultCacheEnabled, showValidatedValuesInTraceLogs, constraintValidatorPayload, constraintExpressionLanguageFeatureLevel, customViolationExpressionLanguageFeatureLevel, @@ -112,7 +112,7 @@ public class ValidatorFactoryScopedContext { temporalValidationTolerance ) ); } - private ValidatorFactoryScopedContext(MessageInterpolator messageInterpolator, + ValidatorFactoryScopedContext(MessageInterpolator messageInterpolator, TraversableResolver traversableResolver, ExecutableParameterNameProvider parameterNameProvider, ClockProvider clockProvider, @@ -121,7 +121,8 @@ private ValidatorFactoryScopedContext(MessageInterpolator messageInterpolator, boolean failFast, boolean failFastOnPropertyViolation, boolean traversableResolverResultCacheEnabled, - boolean showValidatedValuesInTraceLogs, Object constraintValidatorPayload, + boolean showValidatedValuesInTraceLogs, + Object constraintValidatorPayload, ExpressionLanguageFeatureLevel constraintExpressionLanguageFeatureLevel, ExpressionLanguageFeatureLevel customViolationExpressionLanguageFeatureLevel, HibernateConstraintValidatorInitializationContextImpl constraintValidatorInitializationContext) { @@ -212,7 +213,6 @@ static class Builder { private Object constraintValidatorPayload; private ExpressionLanguageFeatureLevel constraintExpressionLanguageFeatureLevel; private ExpressionLanguageFeatureLevel customViolationExpressionLanguageFeatureLevel; - private boolean showValidatedValuesInTraceLogs; private HibernateConstraintValidatorInitializationContextImpl constraintValidatorInitializationContext; diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/path/PathImpl.java b/engine/src/main/java/org/hibernate/validator/internal/engine/path/PathImpl.java index e0ec36458d..8e275afb0f 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/path/PathImpl.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/path/PathImpl.java @@ -204,7 +204,7 @@ private NodeImpl addMethodNode(String name, Class[] parameterTypes) { } public NodeImpl makeLeafNodeIterable() { - requiresWriteableNodeList(); + copyNodeList(); currentLeafNode = NodeImpl.makeIterable( currentLeafNode ); @@ -214,7 +214,7 @@ public NodeImpl makeLeafNodeIterable() { } public NodeImpl makeLeafNodeIterableAndSetIndex(Integer index) { - requiresWriteableNodeList(); + copyNodeList(); currentLeafNode = NodeImpl.makeIterableAndSetIndex( currentLeafNode, index ); @@ -224,7 +224,7 @@ public NodeImpl makeLeafNodeIterableAndSetIndex(Integer index) { } public NodeImpl makeLeafNodeIterableAndSetMapKey(Object key) { - requiresWriteableNodeList(); + copyNodeList(); currentLeafNode = NodeImpl.makeIterableAndSetMapKey( currentLeafNode, key ); @@ -309,6 +309,10 @@ private void requiresWriteableNodeList() { return; } + copyNodeList(); + } + + private void copyNodeList() { // Usually, the write operation is about adding one more node, so let's make the list one element larger. List newNodeList = new ArrayList<>( nodeList.size() + 1 ); newNodeList.addAll( nodeList ); diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/tracking/DefaultProcessedBeansTrackingVoter.java b/engine/src/main/java/org/hibernate/validator/internal/engine/tracking/DefaultProcessedBeansTrackingVoter.java new file mode 100644 index 0000000000..e6384cb1af --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/tracking/DefaultProcessedBeansTrackingVoter.java @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.internal.engine.tracking; + +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter; + +public class DefaultProcessedBeansTrackingVoter implements ProcessedBeansTrackingVoter { + + @Override + public Vote isEnabledForBean(Class beanClass, boolean hasCascadables) { + return Vote.DEFAULT; + } + + @Override + public Vote isEnabledForReturnValue(Class beanClass, String name, Class[] parameterTypes, boolean hasCascadables) { + return Vote.DEFAULT; + } + + @Override + public Vote isEnabledForParameters(Class beanClass, String name, Class[] parameterTypes, boolean hasCascadables) { + return Vote.DEFAULT; + } +} diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/tracking/PredefinedScopeProcessedBeansTrackingStrategy.java b/engine/src/main/java/org/hibernate/validator/internal/engine/tracking/PredefinedScopeProcessedBeansTrackingStrategy.java new file mode 100644 index 0000000000..d76ad0c3dd --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/tracking/PredefinedScopeProcessedBeansTrackingStrategy.java @@ -0,0 +1,235 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.internal.engine.tracking; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.hibernate.validator.internal.metadata.aggregated.BeanMetaData; +import org.hibernate.validator.internal.metadata.aggregated.CascadingMetaData; +import org.hibernate.validator.internal.metadata.aggregated.ContainerCascadingMetaData; +import org.hibernate.validator.internal.metadata.facets.Cascadable; +import org.hibernate.validator.internal.properties.Signature; +import org.hibernate.validator.internal.util.CollectionHelper; + +public class PredefinedScopeProcessedBeansTrackingStrategy implements ProcessedBeansTrackingStrategy { + + private final Map, Boolean> trackingEnabledForBeans; + + private final Map trackingEnabledForReturnValues; + + private final Map trackingEnabledForParameters; + + public PredefinedScopeProcessedBeansTrackingStrategy(Map, BeanMetaData> rawBeanMetaDataMap) { + // TODO: build the maps from the information inside the beanMetaDataManager + // There is a good chance we will need a structure with the whole hierarchy of constraint classes. + // That's something we could add to PredefinedScopeBeanMetaDataManager, as we are already doing similar things + // there (see the ClassHierarchyHelper.getHierarchy call). + // In the predefined scope case, we will have the whole hierarchy of constrained classes passed to + // PredefinedScopeBeanMetaDataManager. + + this.trackingEnabledForBeans = CollectionHelper.toImmutableMap( + new TrackingEnabledStrategyBuilder( rawBeanMetaDataMap ).build() + ); + this.trackingEnabledForReturnValues = CollectionHelper.toImmutableMap( new HashMap<>() ); + this.trackingEnabledForParameters = CollectionHelper.toImmutableMap( new HashMap<>() ); + } + + private static class TrackingEnabledStrategyBuilder { + private final Map, BeanMetaData> rawBeanMetaDataMap; + private final Map, Boolean> classToBeanTrackingEnabled; + + TrackingEnabledStrategyBuilder(Map, BeanMetaData> rawBeanMetaDataMap) { + this.rawBeanMetaDataMap = rawBeanMetaDataMap; + this.classToBeanTrackingEnabled = CollectionHelper.newHashMap( rawBeanMetaDataMap.size() ); + } + + public Map, Boolean> build() { + final Set> beanClassesInPath = new HashSet<>(); + for ( BeanMetaData beanMetadata : rawBeanMetaDataMap.values() ) { + determineTrackingRequired( beanMetadata.getBeanClass(), beanClassesInPath ); + if ( !beanClassesInPath.isEmpty() ) { + throw new IllegalStateException( "beanClassesInPath not empty" ); + } + } + return classToBeanTrackingEnabled; + } + + // Do a depth-first search for cycles along paths of cascaded bean classes. + // The algorithm stops due to one of the following: + // 1) The bean class was previously put in classToBeanTrackingEnabled + // (i.e., the bean class was already determined to either have a cycle, + // or not have a cycle). + // 2) A cycle is found. In this case, all bean classes in the particular path, + // starting from beanClass up to first bean class that causes a cycle, will + // be registered in classToBeanTrackingEnabled with a value of true. + // Once a cycle is found, no further bean classes are examined. Those bean + // classes that were examined in the process that are found to not have a + // cycle are registered in classToBeanTrackingEnabled with a value of false. + // 3) No cycle is found. In this case, all bean classes in the tree will be + // registered in classToBeanTrackingEnabled with a value of false. + // + // Examples: An arrow, ->, indicates a cascading constraint from a bean class. + // + // 1) A -> B + // | ^ + // | | + // ---- + // A, B have no cycles. A has 2 paths to B, but there are no cycles, because there is no path from B to A. + // + // 2) A <- + // | | + // --- + // A has a cycle to itself. + // + // 3) A -> B -> C -> D + // ^ | + // | | + // ----- + // A, B, C have cycles; D does not have a cycle. + // + private boolean determineTrackingRequired(Class beanClass, Set> beanClassesInPath) { + + final Boolean isBeanTrackingEnabled = classToBeanTrackingEnabled.get( beanClass ); + if ( isBeanTrackingEnabled != null ) { + // It was already determined for beanClass. + return isBeanTrackingEnabled; + } + + // Add beanClass to the path. + // We don't care about the order of the bean classes in + // beanClassesInPath. We only care about detecting a duplicate, + // which indicates a cycle. If no cycle is found in beanClass, + // it will be removed below. + if ( !beanClassesInPath.add( beanClass ) ) { + // The bean was already present in the path being examined. + // That means that there is cycle involving beanClass. + // Enable tracking for all elements in beanClassesInPath + for ( Class dependency : beanClassesInPath ) { + register( dependency, true ); + } + beanClassesInPath.clear(); + return true; + } + + // Now check the cascaded bean classes. + for ( Class directCascadedBeanClass : getDirectCascadedBeanClasses( beanClass ) ) { + // Check to see if tracking has already been determined for directCascadedBeanClass + Boolean isSubBeanTrackingEnabled = classToBeanTrackingEnabled.get( directCascadedBeanClass ); + if ( isSubBeanTrackingEnabled != null ) { + if ( isSubBeanTrackingEnabled ) { + // We already know that directCascadedBeanClass has a cycle. + // That means that all elements in beanClassesInPath + // will have a cycle. + for ( Class dependency : beanClassesInPath ) { + register( dependency, true ); + } + // No point in checking any others in this loop. + beanClassesInPath.clear(); + return true; + } + else { + // We already know that directCascadedBeanClass is not involved in + // any cycles, so move on to the next iteration. + continue; + } + } + if ( determineTrackingRequired( directCascadedBeanClass, beanClassesInPath ) ) { + // A cycle was found. No point in checking any others in this loop. + // beanClassesInPath should have already been cleared. + assert beanClassesInPath.isEmpty(); + return true; + } + // directCascadedBeanClass does not have a cycle. + // directCascadedBeanClass would have already been removed by the + // call to #determineTrackingRequired above + } + beanClassesInPath.remove( beanClass ); + return register( beanClass, false ); + } + + // TODO: is there a more concise way to do this? + private Set> getDirectCascadedBeanClasses(Class beanClass) { + final BeanMetaData beanMetaData = rawBeanMetaDataMap.get( beanClass ); + + if ( beanMetaData == null || !beanMetaData.hasCascadables() ) { + return Collections.emptySet(); + } + + final Set> directCascadedBeanClasses = new HashSet<>(); + for ( Cascadable cascadable : beanMetaData.getCascadables() ) { + final CascadingMetaData cascadingMetaData = cascadable.getCascadingMetaData(); + if ( cascadingMetaData.isContainer() ) { + final ContainerCascadingMetaData containerCascadingMetaData = (ContainerCascadingMetaData) cascadingMetaData; + if ( containerCascadingMetaData.getEnclosingType() instanceof ParameterizedType ) { + ParameterizedType parameterizedType = (ParameterizedType) containerCascadingMetaData.getEnclosingType(); + for ( Type typeArgument : parameterizedType.getActualTypeArguments() ) { + if ( typeArgument instanceof Class ) { + directCascadedBeanClasses.add( (Class) typeArgument ); + } + else { + throw new UnsupportedOperationException( "Only ParameterizedType values of type Class are supported" ); + } + } + } + else { + throw new UnsupportedOperationException( "Non-parameterized containers are not supported yet." ); + } + } + else { + // TODO: For now, assume non-container Cascadables are always beans. Truee??? + directCascadedBeanClasses.add( (Class) cascadable.getCascadableType() ); + } + } + return directCascadedBeanClasses; + } + + private boolean register(Class beanClass, boolean isBeanTrackingEnabled) { + if ( classToBeanTrackingEnabled.put( beanClass, isBeanTrackingEnabled ) != null ) { + throw new IllegalStateException( beanClass.getName() + " registered more than once." ); + } + return isBeanTrackingEnabled; + } + } + + @Override + public boolean isEnabledForBean(Class rootBeanClass, boolean hasCascadables) { + if ( !hasCascadables ) { + return false; + } + + return trackingEnabledForBeans.get( rootBeanClass ); + } + + @Override + public boolean isEnabledForReturnValue(Signature signature, boolean hasCascadables) { + if ( !hasCascadables ) { + return false; + } + + return trackingEnabledForReturnValues.getOrDefault( signature, true ); + } + + @Override + public boolean isEnabledForParameters(Signature signature, boolean hasCascadables) { + if ( !hasCascadables ) { + return false; + } + + return trackingEnabledForParameters.getOrDefault( signature, true ); + } + + @Override + public void clear() { + trackingEnabledForBeans.clear(); + trackingEnabledForReturnValues.clear(); + trackingEnabledForParameters.clear(); + } +} diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/tracking/ProcessedBeansTrackingStrategy.java b/engine/src/main/java/org/hibernate/validator/internal/engine/tracking/ProcessedBeansTrackingStrategy.java new file mode 100644 index 0000000000..06df92cae5 --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/tracking/ProcessedBeansTrackingStrategy.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.internal.engine.tracking; + +import org.hibernate.validator.internal.properties.Signature; + +public interface ProcessedBeansTrackingStrategy { + + boolean isEnabledForBean(Class beanClass, boolean hasCascadables); + + boolean isEnabledForReturnValue(Signature signature, boolean hasCascadables); + + boolean isEnabledForParameters(Signature signature, boolean hasCascadables); + + void clear(); +} diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/AbstractValidationContext.java b/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/AbstractValidationContext.java index bef5b4560a..7c0f8a6d62 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/AbstractValidationContext.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/AbstractValidationContext.java @@ -94,7 +94,7 @@ abstract class AbstractValidationContext implements BaseBeanValidationContext /** * Indicates if the tracking of already validated bean should be disabled. */ - private final boolean disableAlreadyValidatedBeanTracking; + private final boolean processedBeanTrackingEnabled; /** * The set of already processed meta constraints per bean - path ({@link BeanPathMetaConstraintProcessedUnit}). @@ -129,7 +129,7 @@ protected AbstractValidationContext( T rootBean, Class rootBeanClass, BeanMetaData rootBeanMetaData, - boolean disableAlreadyValidatedBeanTracking + boolean processedBeanTrackingEnabled ) { this.constraintValidatorManager = constraintValidatorManager; this.validatorScopedContext = validatorScopedContext; @@ -141,7 +141,7 @@ protected AbstractValidationContext( this.rootBeanClass = rootBeanClass; this.rootBeanMetaData = rootBeanMetaData; - this.disableAlreadyValidatedBeanTracking = disableAlreadyValidatedBeanTracking; + this.processedBeanTrackingEnabled = processedBeanTrackingEnabled; } @Override @@ -196,7 +196,7 @@ public ConstraintValidatorFactory getConstraintValidatorFactory() { @Override public boolean isBeanAlreadyValidated(Object value, Class group, PathImpl path) { - if ( disableAlreadyValidatedBeanTracking ) { + if ( !processedBeanTrackingEnabled ) { return false; } @@ -212,7 +212,7 @@ public boolean isBeanAlreadyValidated(Object value, Class group, PathImpl pat @Override public void markCurrentBeanAsProcessed(ValueContext valueContext) { - if ( disableAlreadyValidatedBeanTracking ) { + if ( !processedBeanTrackingEnabled ) { return; } diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/BeanValidationContext.java b/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/BeanValidationContext.java index 806c985eec..f0ac4a8fac 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/BeanValidationContext.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/BeanValidationContext.java @@ -35,14 +35,10 @@ class BeanValidationContext extends AbstractValidationContext { BeanMetaData rootBeanMetaData ) { super( constraintValidatorManager, constraintValidatorFactory, validatorScopedContext, traversableResolver, constraintValidatorInitializationContext, - rootBean, rootBeanClass, rootBeanMetaData, buildDisableAlreadyValidatedBeanTracking( rootBeanMetaData ) + rootBean, rootBeanClass, rootBeanMetaData, rootBeanMetaData.isTrackingEnabled() ); } - private static boolean buildDisableAlreadyValidatedBeanTracking(BeanMetaData rootBeanMetaData) { - return !rootBeanMetaData.hasCascadables(); - } - @Override protected ConstraintViolation createConstraintViolation( String messageTemplate, String interpolatedMessage, Path propertyPath, diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/ParameterExecutableValidationContext.java b/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/ParameterExecutableValidationContext.java index ae7cf85f1c..59af2f2aaf 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/ParameterExecutableValidationContext.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/ParameterExecutableValidationContext.java @@ -66,7 +66,7 @@ public class ParameterExecutableValidationContext extends AbstractValidationC ) { super( constraintValidatorManager, constraintValidatorFactory, validatorScopedContext, traversableResolver, constraintValidatorInitializationContext, rootBean, rootBeanClass, rootBeanMetaData, - buildDisableAlreadyValidatedBeanTracking( executableMetaData ) + isProcessedBeansTrackingEnabled( executableMetaData ) ); this.executable = executable; this.executableMetaData = executableMetaData; @@ -83,13 +83,13 @@ public Optional getExecutableMetaData() { return executableMetaData; } - private static boolean buildDisableAlreadyValidatedBeanTracking(Optional executableMetaData) { + private static boolean isProcessedBeansTrackingEnabled(Optional executableMetaData) { if ( !executableMetaData.isPresent() ) { // the method is unconstrained so there's no need to worry about the tracking return false; } - return !executableMetaData.get().getValidatableParametersMetaData().hasCascadables(); + return executableMetaData.get().isTrackingEnabledForParameters(); } @Override diff --git a/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/ReturnValueExecutableValidationContext.java b/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/ReturnValueExecutableValidationContext.java index e015fcab55..4b20101dd6 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/ReturnValueExecutableValidationContext.java +++ b/engine/src/main/java/org/hibernate/validator/internal/engine/validationcontext/ReturnValueExecutableValidationContext.java @@ -59,7 +59,7 @@ public class ReturnValueExecutableValidationContext extends AbstractValidatio ) { super( constraintValidatorManager, constraintValidatorFactory, validatorScopedContext, traversableResolver, constraintValidatorInitializationContext, rootBean, rootBeanClass, rootBeanMetaData, - buildDisableAlreadyValidatedBeanTracking( executableMetaData ) + isTrackingEnabled( executableMetaData ) ); this.executable = executable; this.executableMetaData = executableMetaData; @@ -76,13 +76,13 @@ public Optional getExecutableMetaData() { return executableMetaData; } - private static boolean buildDisableAlreadyValidatedBeanTracking(Optional executableMetaData) { + private static boolean isTrackingEnabled(Optional executableMetaData) { if ( !executableMetaData.isPresent() ) { // the method is unconstrained so there's no need to worry about the tracking return false; } - return !executableMetaData.get().getReturnValueMetaData().hasCascadables(); + return executableMetaData.get().isTrackingEnabledForReturnValue(); } @Override diff --git a/engine/src/main/java/org/hibernate/validator/internal/metadata/BeanMetaDataManagerImpl.java b/engine/src/main/java/org/hibernate/validator/internal/metadata/BeanMetaDataManagerImpl.java index 1033ff7837..967798728e 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/metadata/BeanMetaDataManagerImpl.java +++ b/engine/src/main/java/org/hibernate/validator/internal/metadata/BeanMetaDataManagerImpl.java @@ -33,6 +33,7 @@ import org.hibernate.validator.internal.util.classhierarchy.ClassHierarchyHelper; import org.hibernate.validator.internal.util.stereotypes.Immutable; import org.hibernate.validator.metadata.BeanMetaDataClassNormalizer; +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter; /** * This manager is in charge of providing all constraint related meta data @@ -95,6 +96,8 @@ public class BeanMetaDataManagerImpl implements BeanMetaDataManager { private final ValidationOrderGenerator validationOrderGenerator; + private final ProcessedBeansTrackingVoter processedBeansTrackingVoter; + /** * the three properties in this field affect the invocation of rules associated to section 4.5.5 * of the specification. By default they are all false, if true they allow @@ -109,14 +112,15 @@ public BeanMetaDataManagerImpl(ConstraintCreationContext constraintCreationConte BeanMetaDataClassNormalizer beanMetaDataClassNormalizer, ValidationOrderGenerator validationOrderGenerator, List optionalMetaDataProviders, - MethodValidationConfiguration methodValidationConfiguration) { + MethodValidationConfiguration methodValidationConfiguration, + ProcessedBeansTrackingVoter processedBeansTrackingVoter) { this.constraintCreationContext = constraintCreationContext; this.executableHelper = executableHelper; this.parameterNameProvider = parameterNameProvider; this.beanMetaDataClassNormalizer = beanMetaDataClassNormalizer; this.validationOrderGenerator = validationOrderGenerator; - this.methodValidationConfiguration = methodValidationConfiguration; + this.processedBeansTrackingVoter = processedBeansTrackingVoter; this.beanMetaDataCache = new ConcurrentReferenceHashMap<>( DEFAULT_INITIAL_CAPACITY, @@ -193,7 +197,8 @@ public int numberOfCachedBeanMetaDataInstances() { private BeanMetaDataImpl createBeanMetaData(Class clazz) { BeanMetaDataBuilder builder = BeanMetaDataBuilder.getInstance( constraintCreationContext, executableHelper, parameterNameProvider, - validationOrderGenerator, clazz, methodValidationConfiguration ); + validationOrderGenerator, clazz, methodValidationConfiguration, + processedBeansTrackingVoter ); for ( MetaDataProvider provider : metaDataProviders ) { for ( BeanConfiguration beanConfiguration : getBeanConfigurationForHierarchy( provider, clazz ) ) { diff --git a/engine/src/main/java/org/hibernate/validator/internal/metadata/PredefinedScopeBeanMetaDataManager.java b/engine/src/main/java/org/hibernate/validator/internal/metadata/PredefinedScopeBeanMetaDataManager.java index 56b312440d..0bc6678e0c 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/metadata/PredefinedScopeBeanMetaDataManager.java +++ b/engine/src/main/java/org/hibernate/validator/internal/metadata/PredefinedScopeBeanMetaDataManager.java @@ -9,9 +9,12 @@ import java.lang.annotation.ElementType; import java.lang.reflect.Executable; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -30,6 +33,8 @@ import org.hibernate.validator.internal.engine.MethodValidationConfiguration; import org.hibernate.validator.internal.engine.groups.Sequence; import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator; +import org.hibernate.validator.internal.engine.tracking.PredefinedScopeProcessedBeansTrackingStrategy; +import org.hibernate.validator.internal.engine.tracking.ProcessedBeansTrackingStrategy; import org.hibernate.validator.internal.metadata.aggregated.BeanMetaData; import org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataBuilder; import org.hibernate.validator.internal.metadata.aggregated.BeanMetaDataImpl; @@ -48,6 +53,7 @@ import org.hibernate.validator.internal.util.classhierarchy.ClassHierarchyHelper; import org.hibernate.validator.internal.util.classhierarchy.Filters; import org.hibernate.validator.metadata.BeanMetaDataClassNormalizer; +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter; public class PredefinedScopeBeanMetaDataManager implements BeanMetaDataManager { @@ -58,7 +64,10 @@ public class PredefinedScopeBeanMetaDataManager implements BeanMetaDataManager { */ private final ConcurrentMap, BeanMetaData> beanMetaDataMap = new ConcurrentHashMap<>(); - public PredefinedScopeBeanMetaDataManager(ConstraintCreationContext constraintCreationContext, + private final ProcessedBeansTrackingStrategy processedBeansTrackingStrategy; + + public PredefinedScopeBeanMetaDataManager( + ConstraintCreationContext constraintCreationContext, ExecutableHelper executableHelper, ExecutableParameterNameProvider parameterNameProvider, JavaBeanHelper javaBeanHelper, @@ -66,7 +75,9 @@ public PredefinedScopeBeanMetaDataManager(ConstraintCreationContext constraintCr List optionalMetaDataProviders, MethodValidationConfiguration methodValidationConfiguration, BeanMetaDataClassNormalizer beanMetaDataClassNormalizer, - Set> beanClassesToInitialize) { + ProcessedBeansTrackingVoter processedBeansTrackingVoter, + Set> beanClassesToInitialize + ) { AnnotationProcessingOptions annotationProcessingOptions = getAnnotationProcessingOptionsFromNonDefaultProviders( optionalMetaDataProviders ); AnnotationMetaDataProvider defaultProvider = new AnnotationMetaDataProvider( constraintCreationContext, @@ -82,27 +93,45 @@ public PredefinedScopeBeanMetaDataManager(ConstraintCreationContext constraintCr metaDataProviders.add( defaultProvider ); metaDataProviders.addAll( optionalMetaDataProviders ); + Map, BeanMetaData> rawBeanMetaDataMap = new HashMap<>(); for ( Class validatedClass : beanClassesToInitialize ) { Class normalizedValidatedClass = beanMetaDataClassNormalizer.normalize( validatedClass ); @SuppressWarnings("unchecked") - List> classHierarchy = (List>) (Object) ClassHierarchyHelper.getHierarchy( normalizedValidatedClass, - Filters.excludeInterfaces( normalizedValidatedClass ) ); + List> classHierarchy = (List>) (Object) ClassHierarchyHelper.getHierarchy( + normalizedValidatedClass, + Filters.excludeInterfaces( normalizedValidatedClass ) + ); // note that the hierarchy also contains the initial class for ( Class hierarchyElement : classHierarchy ) { - if ( this.beanMetaDataMap.containsKey( hierarchyElement ) ) { + if ( rawBeanMetaDataMap.containsKey( hierarchyElement ) ) { continue; } - this.beanMetaDataMap.put( hierarchyElement, - createBeanMetaData( constraintCreationContext, executableHelper, parameterNameProvider, + rawBeanMetaDataMap.put( + hierarchyElement, + createBeanMetaData( + constraintCreationContext, executableHelper, parameterNameProvider, javaBeanHelper, validationOrderGenerator, optionalMetaDataProviders, methodValidationConfiguration, - metaDataProviders, hierarchyElement ) ); + processedBeansTrackingVoter, metaDataProviders, hierarchyElement + ) + ); } } this.beanMetaDataClassNormalizer = beanMetaDataClassNormalizer; + this.processedBeansTrackingStrategy = new PredefinedScopeProcessedBeansTrackingStrategy( + rawBeanMetaDataMap + ); + + // Inject the processed beans tracking information into the BeanMetaData objects + for ( Map.Entry, BeanMetaData> rawBeanMetaDataEntry : rawBeanMetaDataMap.entrySet() ) { + beanMetaDataMap.put( + rawBeanMetaDataEntry.getKey(), + injectTrackingInformation( rawBeanMetaDataEntry.getValue(), processedBeansTrackingStrategy, processedBeansTrackingVoter ) + ); + } } @SuppressWarnings("unchecked") @@ -118,6 +147,14 @@ public BeanMetaData getBeanMetaData(Class beanClass) { return beanMetaData; } + public Collection> getBeanMetaData() { + return beanMetaDataMap.values(); + } + + public ProcessedBeansTrackingStrategy getProcessedBeansTrackingStrategy() { + return processedBeansTrackingStrategy; + } + @Override public void clear() { beanMetaDataMap.clear(); @@ -129,21 +166,25 @@ public void clear() { * * @param The type of interest. * @param clazz The type's class. - * * @return A bean meta data object for the given type. */ - private static BeanMetaDataImpl createBeanMetaData(ConstraintCreationContext constraintCreationContext, + private static BeanMetaDataImpl createBeanMetaData( + ConstraintCreationContext constraintCreationContext, ExecutableHelper executableHelper, ExecutableParameterNameProvider parameterNameProvider, JavaBeanHelper javaBeanHelper, ValidationOrderGenerator validationOrderGenerator, List optionalMetaDataProviders, MethodValidationConfiguration methodValidationConfiguration, + ProcessedBeansTrackingVoter processedBeansTrackingVoter, List metaDataProviders, - Class clazz) { + Class clazz + ) { BeanMetaDataBuilder builder = BeanMetaDataBuilder.getInstance( constraintCreationContext, executableHelper, parameterNameProvider, - validationOrderGenerator, clazz, methodValidationConfiguration ); + validationOrderGenerator, clazz, methodValidationConfiguration, + processedBeansTrackingVoter + ); for ( MetaDataProvider provider : metaDataProviders ) { for ( BeanConfiguration beanConfiguration : getBeanConfigurationForHierarchy( provider, clazz ) ) { @@ -188,6 +229,14 @@ private static List> getBeanConfigurationForHie return configurations; } + private static BeanMetaData injectTrackingInformation( + BeanMetaData rawBeanMetaData, + ProcessedBeansTrackingStrategy processedBeansTrackingStrategy, + ProcessedBeansTrackingVoter processedBeansTrackingVoter + ) { + return new BeanMetaDataImpl( (BeanMetaDataImpl) rawBeanMetaData, processedBeansTrackingStrategy, processedBeansTrackingVoter ); + } + private static class UninitializedBeanMetaData implements BeanMetaData { private final Class beanClass; @@ -287,6 +336,11 @@ public Optional getMetaDataFor(Executable executable) throws public List> getClassHierarchy() { return classHierarchy; } + + @Override + public boolean isTrackingEnabled() { + return true; + } } private static class UninitializedBeanDescriptor implements BeanDescriptor { diff --git a/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/AbstractConstraintMetaData.java b/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/AbstractConstraintMetaData.java index d28d01e352..7614cfe6a8 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/AbstractConstraintMetaData.java +++ b/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/AbstractConstraintMetaData.java @@ -64,6 +64,16 @@ public AbstractConstraintMetaData(String name, Type type, Set> this.isConstrained = isConstrained; } + protected AbstractConstraintMetaData(AbstractConstraintMetaData originalAbstractConstraintMetaData) { + this.name = originalAbstractConstraintMetaData.name; + this.type = originalAbstractConstraintMetaData.type; + this.directConstraints = originalAbstractConstraintMetaData.directConstraints; + this.containerElementsConstraints = originalAbstractConstraintMetaData.containerElementsConstraints; + this.allConstraints = originalAbstractConstraintMetaData.allConstraints; + this.isCascading = originalAbstractConstraintMetaData.isCascading; + this.isConstrained = originalAbstractConstraintMetaData.isConstrained; + } + @Override public String getName() { return name; diff --git a/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaData.java b/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaData.java index aae63e0f50..12487ae063 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaData.java +++ b/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaData.java @@ -137,4 +137,9 @@ public interface BeanMetaData extends Validatable { * element itself and goes up the hierarchy chain. Interfaces are not included. */ List> getClassHierarchy(); + + /** + * @return {@code true} if the bean class is required to be tracked; {@code false} otherwise. + */ + boolean isTrackingEnabled(); } diff --git a/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaDataBuilder.java b/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaDataBuilder.java index 9715d5b86c..40f915117e 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaDataBuilder.java +++ b/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaDataBuilder.java @@ -23,6 +23,7 @@ import org.hibernate.validator.internal.util.ExecutableParameterNameProvider; import org.hibernate.validator.internal.util.StringHelper; import org.hibernate.validator.spi.group.DefaultGroupSequenceProvider; +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter; /** * @author Hardy Ferentschik @@ -41,6 +42,7 @@ public class BeanMetaDataBuilder { private final ExecutableHelper executableHelper; private final ExecutableParameterNameProvider parameterNameProvider; private final MethodValidationConfiguration methodValidationConfiguration; + private ProcessedBeansTrackingVoter processedBeansTrackingVoter; private ConfigurationSource sequenceSource; private ConfigurationSource providerSource; @@ -54,13 +56,15 @@ private BeanMetaDataBuilder( ExecutableParameterNameProvider parameterNameProvider, ValidationOrderGenerator validationOrderGenerator, Class beanClass, - MethodValidationConfiguration methodValidationConfiguration) { + MethodValidationConfiguration methodValidationConfiguration, + ProcessedBeansTrackingVoter processedBeansTrackingVoter) { this.beanClass = beanClass; this.constraintCreationContext = constraintCreationContext; this.validationOrderGenerator = validationOrderGenerator; this.executableHelper = executableHelper; this.parameterNameProvider = parameterNameProvider; this.methodValidationConfiguration = methodValidationConfiguration; + this.processedBeansTrackingVoter = processedBeansTrackingVoter; } public static BeanMetaDataBuilder getInstance( @@ -69,14 +73,16 @@ public static BeanMetaDataBuilder getInstance( ExecutableParameterNameProvider parameterNameProvider, ValidationOrderGenerator validationOrderGenerator, Class beanClass, - MethodValidationConfiguration methodValidationConfiguration) { + MethodValidationConfiguration methodValidationConfiguration, + ProcessedBeansTrackingVoter processedBeansTrackingVoter) { return new BeanMetaDataBuilder<>( constraintCreationContext, executableHelper, parameterNameProvider, validationOrderGenerator, beanClass, - methodValidationConfiguration ); + methodValidationConfiguration, + processedBeansTrackingVoter ); } public void add(BeanConfiguration configuration) { @@ -117,7 +123,8 @@ private void addMetaDataToBuilder(ConstrainedElement constrainableElement, Set build() { defaultGroupSequence, defaultGroupSequenceProvider, aggregatedElements, - validationOrderGenerator + validationOrderGenerator, + processedBeansTrackingVoter ); } @@ -147,6 +155,7 @@ private static class BuilderDelegate { private MetaDataBuilder metaDataBuilder; private ExecutableMetaData.Builder methodBuilder; private final MethodValidationConfiguration methodValidationConfiguration; + private final ProcessedBeansTrackingVoter processedBeansTrackingVoter; private final int hashCode; public BuilderDelegate( @@ -155,7 +164,8 @@ public BuilderDelegate( ConstraintCreationContext constraintCreationContext, ExecutableHelper executableHelper, ExecutableParameterNameProvider parameterNameProvider, - MethodValidationConfiguration methodValidationConfiguration + MethodValidationConfiguration methodValidationConfiguration, + ProcessedBeansTrackingVoter processedBeansTrackingVoter ) { this.beanClass = beanClass; this.constrainedElement = constrainedElement; @@ -163,6 +173,7 @@ public BuilderDelegate( this.executableHelper = executableHelper; this.parameterNameProvider = parameterNameProvider; this.methodValidationConfiguration = methodValidationConfiguration; + this.processedBeansTrackingVoter = processedBeansTrackingVoter; switch ( constrainedElement.getKind() ) { case FIELD: @@ -188,7 +199,8 @@ public BuilderDelegate( constraintCreationContext, executableHelper, parameterNameProvider, - methodValidationConfiguration + methodValidationConfiguration, + processedBeansTrackingVoter ); } @@ -235,7 +247,8 @@ public boolean add(ConstrainedElement constrainedElement) { constraintCreationContext, executableHelper, parameterNameProvider, - methodValidationConfiguration + methodValidationConfiguration, + processedBeansTrackingVoter ); } diff --git a/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaDataImpl.java b/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaDataImpl.java index e1b7a76015..1069b571c6 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaDataImpl.java +++ b/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/BeanMetaDataImpl.java @@ -29,6 +29,7 @@ import org.hibernate.validator.internal.engine.groups.Sequence; import org.hibernate.validator.internal.engine.groups.ValidationOrder; import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator; +import org.hibernate.validator.internal.engine.tracking.ProcessedBeansTrackingStrategy; import org.hibernate.validator.internal.metadata.core.MetaConstraint; import org.hibernate.validator.internal.metadata.descriptor.BeanDescriptorImpl; import org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl; @@ -43,6 +44,8 @@ import org.hibernate.validator.internal.util.logging.LoggerFactory; import org.hibernate.validator.internal.util.stereotypes.Immutable; import org.hibernate.validator.spi.group.DefaultGroupSequenceProvider; +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter; +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter.Vote; /** * This class encapsulates all meta data needed for validation. Implementations of {@code Validator} interface can @@ -183,6 +186,11 @@ public final class BeanMetaDataImpl implements BeanMetaData { */ private volatile BeanDescriptor beanDescriptor; + /** + * Whether tracking of processed beans should be enabled for objects of this type. + */ + private final boolean trackingEnabled; + /** * Creates a new {@link BeanMetaDataImpl} * @@ -195,7 +203,8 @@ public BeanMetaDataImpl(Class beanClass, List> defaultGroupSequence, DefaultGroupSequenceProvider defaultGroupSequenceProvider, Set constraintMetaDataSet, - ValidationOrderGenerator validationOrderGenerator) { + ValidationOrderGenerator validationOrderGenerator, + ProcessedBeansTrackingVoter processedBeansTrackingVoter) { this.validationOrderGenerator = validationOrderGenerator; this.beanClass = beanClass; @@ -270,6 +279,60 @@ else if ( constraintMetaData.getKind() == ElementKind.BEAN ) { // We initialize those elements eagerly so that any eventual error is thrown when bootstrapping the bean metadata this.defaultGroupSequenceRedefined = this.defaultGroupSequence.size() > 1 || hasDefaultGroupSequenceProvider(); this.resolvedDefaultGroupSequence = getDefaultGroupSequence( null ); + + Vote processedBeansTrackingVote = processedBeansTrackingVoter.isEnabledForBean( beanClass, hasCascadables() ); + switch ( processedBeansTrackingVote ) { + case NON_TRACKING: + this.trackingEnabled = false; + break; + case TRACKING: + this.trackingEnabled = true; + break; + default: + this.trackingEnabled = hasCascadables(); + break; + } + } + + public BeanMetaDataImpl(BeanMetaDataImpl originalBeanMetaData, ProcessedBeansTrackingStrategy processedBeansTrackingStrategy, + ProcessedBeansTrackingVoter processedBeansTrackingVoter) { + this.validationOrderGenerator = originalBeanMetaData.validationOrderGenerator; + this.beanClass = originalBeanMetaData.beanClass; + this.propertyMetaDataMap = originalBeanMetaData.propertyMetaDataMap; + this.hasConstraints = originalBeanMetaData.hasConstraints; + this.cascadedProperties = originalBeanMetaData.cascadedProperties; + this.allMetaConstraints = originalBeanMetaData.allMetaConstraints; + this.allDirectMetaConstraints = originalBeanMetaData.allDirectMetaConstraints; + this.classMetaConstraints = originalBeanMetaData.classMetaConstraints; + this.propertyMetaConstraints = originalBeanMetaData.propertyMetaConstraints; + this.directClassMetaConstraints = originalBeanMetaData.directClassMetaConstraints; + this.directPropertyMetaConstraints = originalBeanMetaData.directPropertyMetaConstraints; + this.classHierarchyWithoutInterfaces = originalBeanMetaData.classHierarchyWithoutInterfaces; + this.defaultGroupSequenceProvider = originalBeanMetaData.defaultGroupSequenceProvider; + this.defaultGroupSequence = originalBeanMetaData.defaultGroupSequence; + this.validationOrder = originalBeanMetaData.validationOrder; + Map tempExecutableMetaDataMap = newHashMap(); + for ( Entry executableMetaDataEntry : originalBeanMetaData.executableMetaDataMap.entrySet() ) { + tempExecutableMetaDataMap.put( executableMetaDataEntry.getKey(), + new ExecutableMetaData( executableMetaDataEntry.getValue(), processedBeansTrackingStrategy, processedBeansTrackingVoter ) ); + } + this.executableMetaDataMap = CollectionHelper.toImmutableMap( tempExecutableMetaDataMap ); + this.unconstrainedExecutables = originalBeanMetaData.unconstrainedExecutables; + this.defaultGroupSequenceRedefined = originalBeanMetaData.defaultGroupSequenceRedefined; + this.resolvedDefaultGroupSequence = originalBeanMetaData.resolvedDefaultGroupSequence; + + Vote processedBeansTrackingVote = processedBeansTrackingVoter.isEnabledForBean( beanClass, hasCascadables() ); + switch ( processedBeansTrackingVote ) { + case NON_TRACKING: + this.trackingEnabled = false; + break; + case TRACKING: + this.trackingEnabled = true; + break; + default: + this.trackingEnabled = processedBeansTrackingStrategy.isEnabledForBean( this.beanClass, hasCascadables() ); + break; + } } @Override @@ -409,6 +472,11 @@ public List> getClassHierarchy() { return classHierarchyWithoutInterfaces; } + @Override + public boolean isTrackingEnabled() { + return trackingEnabled; + } + private static BeanDescriptor createBeanDescriptor(Class beanClass, Set> classMetaConstraints, Map propertyMetaDataMap, Map executableMetaDataMap, boolean defaultGroupSequenceRedefined, diff --git a/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/ExecutableMetaData.java b/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/ExecutableMetaData.java index 19bd9c0c08..3ee5ff9f65 100644 --- a/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/ExecutableMetaData.java +++ b/engine/src/main/java/org/hibernate/validator/internal/metadata/aggregated/ExecutableMetaData.java @@ -21,6 +21,7 @@ import org.hibernate.validator.internal.engine.ConstraintCreationContext; import org.hibernate.validator.internal.engine.MethodValidationConfiguration; +import org.hibernate.validator.internal.engine.tracking.ProcessedBeansTrackingStrategy; import org.hibernate.validator.internal.metadata.aggregated.rule.MethodConfigurationRule; import org.hibernate.validator.internal.metadata.core.MetaConstraint; import org.hibernate.validator.internal.metadata.descriptor.ExecutableDescriptorImpl; @@ -34,6 +35,8 @@ import org.hibernate.validator.internal.util.ExecutableHelper; import org.hibernate.validator.internal.util.ExecutableParameterNameProvider; import org.hibernate.validator.internal.util.stereotypes.Immutable; +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter; +import org.hibernate.validator.spi.tracking.ProcessedBeansTrackingVoter.Vote; /** * An aggregated view of the constraint related meta data for a given method or @@ -54,6 +57,8 @@ */ public class ExecutableMetaData extends AbstractConstraintMetaData { + private final Class beanClass; + private final Class[] parameterTypes; @Immutable @@ -76,7 +81,11 @@ public class ExecutableMetaData extends AbstractConstraintMetaData { private final ReturnValueMetaData returnValueMetaData; private final ElementKind kind; + private final boolean trackingEnabledForParameters; + private final boolean trackingEnabledForReturnValue; + private ExecutableMetaData( + Class beanClass, String name, Type returnType, Class[] parameterTypes, @@ -88,7 +97,8 @@ private ExecutableMetaData( Set> crossParameterConstraints, CascadingMetaData cascadingMetaData, boolean isConstrained, - boolean isGetter) { + boolean isGetter, + ProcessedBeansTrackingVoter processedBeansTrackingVoter) { super( name, returnType, @@ -98,6 +108,7 @@ private ExecutableMetaData( isConstrained ); + this.beanClass = beanClass; this.parameterTypes = parameterTypes; this.parameterMetaDataList = CollectionHelper.toImmutableList( parameterMetaDataList ); this.validatableParametersMetaData = new ValidatableParametersMetaData( parameterMetaDataList ); @@ -111,6 +122,90 @@ private ExecutableMetaData( ); this.isGetter = isGetter; this.kind = kind; + + Vote processedBeansTrackingVoteForParameters = processedBeansTrackingVoter.isEnabledForParameters( beanClass, name, parameterTypes, + validatableParametersMetaData.hasCascadables() ); + switch ( processedBeansTrackingVoteForParameters ) { + case NON_TRACKING: + this.trackingEnabledForParameters = false; + break; + case TRACKING: + this.trackingEnabledForParameters = true; + break; + default: + this.trackingEnabledForParameters = validatableParametersMetaData.hasCascadables(); + break; + } + + Vote processedBeansTrackingVoteForReturnValue = processedBeansTrackingVoter.isEnabledForReturnValue( beanClass, name, parameterTypes, + returnValueMetaData.hasCascadables() ); + switch ( processedBeansTrackingVoteForReturnValue ) { + case NON_TRACKING: + this.trackingEnabledForReturnValue = false; + break; + case TRACKING: + this.trackingEnabledForReturnValue = true; + break; + default: + this.trackingEnabledForReturnValue = returnValueMetaData.hasCascadables(); + break; + } + } + + public ExecutableMetaData(ExecutableMetaData originalExecutableMetaData, + ProcessedBeansTrackingStrategy processedBeansTrackingStrategy, + ProcessedBeansTrackingVoter processedBeansTrackingVoter) { + super( originalExecutableMetaData ); + + this.beanClass = originalExecutableMetaData.beanClass; + this.parameterTypes = originalExecutableMetaData.parameterTypes; + this.parameterMetaDataList = originalExecutableMetaData.parameterMetaDataList; + this.validatableParametersMetaData = originalExecutableMetaData.validatableParametersMetaData; + this.crossParameterConstraints = originalExecutableMetaData.crossParameterConstraints; + this.signatures = originalExecutableMetaData.signatures; + this.returnValueMetaData = originalExecutableMetaData.returnValueMetaData; + this.isGetter = originalExecutableMetaData.isGetter; + this.kind = originalExecutableMetaData.kind; + + Vote processedBeansTrackingVoteForParameters = processedBeansTrackingVoter.isEnabledForParameters( originalExecutableMetaData.beanClass, + originalExecutableMetaData.getName(), parameterTypes, originalExecutableMetaData.getValidatableParametersMetaData().hasCascadables() ); + switch ( processedBeansTrackingVoteForParameters ) { + case NON_TRACKING: + this.trackingEnabledForParameters = false; + break; + case TRACKING: + this.trackingEnabledForParameters = true; + break; + default: + boolean trackingEnabledForParameters = false; + for ( Signature signature : originalExecutableMetaData.getSignatures() ) { + trackingEnabledForParameters = trackingEnabledForParameters + || processedBeansTrackingStrategy.isEnabledForParameters( signature, + originalExecutableMetaData.getValidatableParametersMetaData().hasCascadables() ); + } + this.trackingEnabledForParameters = trackingEnabledForParameters; + break; + } + + Vote processedBeansTrackingVoteForReturnValue = processedBeansTrackingVoter.isEnabledForReturnValue( originalExecutableMetaData.beanClass, + originalExecutableMetaData.getName(), parameterTypes, originalExecutableMetaData.getReturnValueMetaData().hasCascadables() ); + switch ( processedBeansTrackingVoteForReturnValue ) { + case NON_TRACKING: + this.trackingEnabledForReturnValue = false; + break; + case TRACKING: + this.trackingEnabledForReturnValue = true; + break; + default: + boolean trackingEnabledForReturnValue = false; + for ( Signature signature : originalExecutableMetaData.getSignatures() ) { + trackingEnabledForReturnValue = trackingEnabledForReturnValue + || processedBeansTrackingStrategy.isEnabledForReturnValue( signature, + originalExecutableMetaData.getReturnValueMetaData().hasCascadables() ); + } + this.trackingEnabledForReturnValue = trackingEnabledForReturnValue; + break; + } } /** @@ -197,6 +292,15 @@ public ElementKind getKind() { return kind; } + + public boolean isTrackingEnabledForParameters() { + return trackingEnabledForParameters; + } + + public boolean isTrackingEnabledForReturnValue() { + return trackingEnabledForReturnValue; + } + @Override public String toString() { StringBuilder parameterBuilder = new StringBuilder(); @@ -258,6 +362,7 @@ public static class Builder extends MetaDataBuilder { private final Set rules; private boolean isConstrained = false; private CascadingMetaDataBuilder cascadingMetaDataBuilder; + private ProcessedBeansTrackingVoter processedBeansTrackingVoter; /** * Holds a merged representation of the configurations for one method @@ -277,11 +382,13 @@ public Builder( ConstraintCreationContext constraintCreationContext, ExecutableHelper executableHelper, ExecutableParameterNameProvider parameterNameProvider, - MethodValidationConfiguration methodValidationConfiguration) { + MethodValidationConfiguration methodValidationConfiguration, + ProcessedBeansTrackingVoter processedBeansTrackingVoter) { super( beanClass, constraintCreationContext ); this.executableHelper = executableHelper; this.parameterNameProvider = parameterNameProvider; + this.processedBeansTrackingVoter = processedBeansTrackingVoter; this.kind = constrainedExecutable.getKind(); this.callable = constrainedExecutable.getCallable(); this.rules = methodValidationConfiguration.getConfiguredRuleSet(); @@ -373,6 +480,7 @@ public ExecutableMetaData build() { assertCorrectnessOfConfiguration(); return new ExecutableMetaData( + callable.getDeclaringClass(), callable.getName(), callable.getType(), callable.getParameterTypes(), @@ -384,7 +492,8 @@ public ExecutableMetaData build() { adaptOriginsAndImplicitGroups( crossParameterConstraints ), cascadingMetaDataBuilder.build( constraintCreationContext.getValueExtractorManager(), callable ), isConstrained, - kind == ConstrainedElementKind.GETTER + kind == ConstrainedElementKind.GETTER, + processedBeansTrackingVoter ); } diff --git a/engine/src/main/java/org/hibernate/validator/spi/tracking/ProcessedBeansTrackingVoter.java b/engine/src/main/java/org/hibernate/validator/spi/tracking/ProcessedBeansTrackingVoter.java new file mode 100644 index 0000000000..7529b7c172 --- /dev/null +++ b/engine/src/main/java/org/hibernate/validator/spi/tracking/ProcessedBeansTrackingVoter.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.spi.tracking; + +import org.hibernate.validator.Incubating; + +@Incubating +public interface ProcessedBeansTrackingVoter { + + Vote isEnabledForBean(Class beanClass, boolean hasCascadables); + + Vote isEnabledForReturnValue(Class beanClass, String name, Class[] parameterTypes, boolean hasCascadables); + + Vote isEnabledForParameters(Class beanClass, String name, Class[] parameterTypes, boolean hasCascadables); + + enum Vote { + + DEFAULT, NON_TRACKING, TRACKING; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/path/PathImplTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/path/PathImplTest.java index f06fe1a2c1..706a14eac1 100644 --- a/engine/src/test/java/org/hibernate/validator/test/internal/engine/path/PathImplTest.java +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/path/PathImplTest.java @@ -31,6 +31,7 @@ import org.hibernate.validator.internal.engine.MethodValidationConfiguration; import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator; import org.hibernate.validator.internal.engine.path.PathImpl; +import org.hibernate.validator.internal.engine.tracking.DefaultProcessedBeansTrackingVoter; import org.hibernate.validator.internal.metadata.BeanMetaDataManager; import org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl; import org.hibernate.validator.internal.metadata.DefaultBeanMetaDataClassNormalizer; @@ -242,7 +243,8 @@ public void testCreationOfExecutablePath() throws Exception { new DefaultBeanMetaDataClassNormalizer(), new ValidationOrderGenerator(), Collections.emptyList(), - new MethodValidationConfiguration.Builder().build() + new MethodValidationConfiguration.Builder().build(), + new DefaultProcessedBeansTrackingVoter() ); ExecutableMetaData executableMetaData = beanMetaDataManager.getBeanMetaData( Container.class ) diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles1Test.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles1Test.java new file mode 100644 index 0000000000..d4601f0f53 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles1Test.java @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.PredefinedScopeHibernateValidator; + +import org.testng.annotations.Test; + +/** + * This is not a real test, just an illustration. + *

+ * This is the most simple example. + * + * @author Guillaume Smet + */ +public class ProcessedBeansTrackingCycles1Test { + + @Test + public void testValidNull() throws Exception { + final Parent parent = new Parent( "parent property" ); + Set> violations = getValidator().validate( parent ); + assertTrue( violations.isEmpty() ); + } + + @Test + public void testValidNotNull() throws Exception { + final Parent parent = new Parent( "parent property" ); + parent.child = new Child( "child property" ); + + Set> violations = getValidator().validate( parent ); + //Set> violations = ValidatorUtil.getValidator().validate( parent ); + assertTrue( violations.isEmpty() ); + } + + @Test + public void testValidNotNullNonCyclic() throws Exception { + final Parent parent = new Parent( "parent property" ); + parent.child = new Child( "child property" ); + parent.child.parent = new Parent( "other parent property" ); + + Set> violations = getValidator().validate( parent ); + assertTrue( violations.isEmpty() ); + } + + @Test + public void testValidNotNullCyclic() throws Exception { + final Parent parent = new Parent( "parent property" ); + parent.child = new Child( "child property" ); + parent.child.parent = parent; + + Set> violations = getValidator().validate( parent ); + assertTrue( violations.isEmpty() ); + } + + private Validator getValidator() { + return Validation.byProvider( PredefinedScopeHibernateValidator.class ) + .configure() + .builtinConstraints( new HashSet<>( Arrays.asList( NotNull.class.getName() ) ) ) + .initializeBeanMetaData( new HashSet<>( Arrays.asList( Parent.class, Child.class, Other.class ) ) ) + .buildValidatorFactory() + .getValidator(); + } + + private static class Parent { + + Parent(String property) { + this.property = property; + } + + @NotNull + private String property; + + @Valid + private Child child; + } + + private static class Child { + + Child(String property) { + this.property = property; + } + + @NotNull + private String property; + + @Valid + private Parent parent; + + @Valid + private Other other; + } + + private static class Other { + Other(String property) { + this.property = property; + } + + @NotNull + private String property; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles2Test.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles2Test.java new file mode 100644 index 0000000000..bd7ca0e78b --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles2Test.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import java.util.List; + +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.testutils.ValidatorUtil; + +import org.testng.annotations.Test; + +/** + * This is not a real test, just an illustration. + *

+ * Simple enough but this time the cascading annotation is in the container element. + * + * @author Guillaume Smet + */ +public class ProcessedBeansTrackingCycles2Test { + + @Test + public void testSerializeHibernateEmail() throws Exception { + Validator validator = ValidatorUtil.getValidator(); + + validator.validate( new Parent() ); + } + + private static class Parent { + + @NotNull + private String property; + + private List<@Valid Child> children; + } + + private static class Child { + + @NotNull + private String property; + + @Valid + private Parent parent; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles3Test.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles3Test.java new file mode 100644 index 0000000000..8d5452e936 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles3Test.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import java.util.List; +import java.util.Map; + +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.testutils.ValidatorUtil; + +import org.testng.annotations.Test; + +/** + * This is not a real test, just an illustration. + *

+ * Simple enough but this time the cascading annotation is deep in a container element. + * + * @author Guillaume Smet + */ +public class ProcessedBeansTrackingCycles3Test { + + @Test + public void testSerializeHibernateEmail() throws Exception { + Validator validator = ValidatorUtil.getValidator(); + + validator.validate( new Parent() ); + } + + private static class Parent { + + @NotNull + private String property; + + private Map> children; + } + + private static class Child { + + @NotNull + private String property; + + @Valid + private Parent parent; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles4Test.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles4Test.java new file mode 100644 index 0000000000..6752d25512 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles4Test.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import java.util.List; +import java.util.Map; + +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.testutils.ValidatorUtil; + +import org.testng.annotations.Test; + +/** + * This is not a real test, just an illustration. + *

+ * Simple enough but this time the cascading annotation is deep in a container element with a bound. + * + * @author Guillaume Smet + */ +public class ProcessedBeansTrackingCycles4Test { + + @Test + public void testSerializeHibernateEmail() throws Exception { + Validator validator = ValidatorUtil.getValidator(); + + validator.validate( new Parent() ); + } + + private static class Parent { + + @NotNull + private String property; + + private Map> children; + } + + private static class Child { + + @NotNull + private String property; + + @Valid + private Parent parent; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles5Test.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles5Test.java new file mode 100644 index 0000000000..f8e2c7e740 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCycles5Test.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import java.util.List; + +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.testutils.ValidatorUtil; + +import org.testng.annotations.Test; + +/** + * This is not a real test, just an illustration. + *

+ * This one is a bit more tricky: during the validation, when cascading, we take into account the runtime type to get + * the metadata, not the declared type. + *

+ * So even if you couldn't have a cycle with the declared type, when trying to find the cycles, we need to take into + * consideration all the subclasses too. The good news is that we are in a closed world so we have them all passed + * to our PredefinedScopedValidatorFactoryImpl! + * + * @author Guillaume Smet + */ +public class ProcessedBeansTrackingCycles5Test { + + @Test + public void testSerializeHibernateEmail() throws Exception { + Validator validator = ValidatorUtil.getValidator(); + + validator.validate( new Parent() ); + } + + private static class Parent { + + @NotNull + private String property; + + private List<@Valid ChildWithNoCycles> children; + } + + private static class ChildWithNoCycles { + + @NotNull + private String property; + } + + private static class Child extends ChildWithNoCycles { + + @Valid + private Parent parent; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesListDuplicateElementsTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesListDuplicateElementsTest.java new file mode 100644 index 0000000000..11d810c5cb --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesListDuplicateElementsTest.java @@ -0,0 +1,205 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.PredefinedScopeHibernateValidator; +import org.hibernate.validator.PredefinedScopeHibernateValidatorFactory; +import org.hibernate.validator.internal.engine.PredefinedScopeValidatorFactoryImpl; +import org.hibernate.validator.internal.engine.tracking.ProcessedBeansTrackingStrategy; + +import org.testng.annotations.Test; + +/** + * An example of beans with cascading constraints, some cycle and others do not. + * + * A -> B ---> C ------> F -> G <- + * | ^ | ^ ^ | + * | | | | | | + * | -- D <-- | | | + * --------------------> E ------- + * + * A, B, C, D, E, F, and G are beans that get validated. + * + * An arrow, ->, indicates a cascading constraint. + * + * The following are the properties with cascading List constraints: + * A.bValues + * .eValues + * B.cValues + * C.dValues + * .fValues + * D.bValues + * E.fValues + * .gValues + * .gAnotherValues + * F.gValues + * + * @author Gail Badner + * + */ + +public class ProcessedBeansTrackingCyclesNoCyclesListDuplicateElementsTest { + + @Test + public void testTrackingEnabled() { + final ProcessedBeansTrackingStrategy processedBeansTrackingStrategy = getProcessedBeansTrackingStrategy(); + + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + A.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + B.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + C.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + D.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + E.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + F.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + G.class, + false + ) ); + } + + @Test + public void testValidate() { + final A a = new A(); + final B b = new B(); + final C c = new C(); + final D d = new D(); + final E e = new E(); + final F f = new F(); + final G g = new G(); + + a.bValues.add( b ); + a.bValues.add( b ); + a.eValues.add( e ); + a.eValues.add( e ); + b.cValues.add( c ); + b.cValues.add( c ); + c.dValues.add( d ); + c.dValues.add( d ); + d.bValues.add( b ); + d.bValues.add( b ); + e.fValues.add( f ); + e.fValues.add( f ); + e.gValues.add( g ); + e.gValues.add( g ); + e.gAnotherValues.add( g ); + e.gAnotherValues.add( g ); + f.gValues.add( g ); + f.gValues.add( g ); + + final Validator validator = getValidator(); + final Set> violationsA = validator.validate( a ); + final Set> violationsB = validator.validate( b ); + final Set> violationsC = validator.validate( c ); + final Set> violationsD = validator.validate( d ); + final Set> violationsE = validator.validate( e ); + final Set> violationsF = validator.validate( f ); + final Set> violationsG = validator.validate( g ); + } + + private Validator getValidator() { + return getValidatorFactory().getValidator(); + } + + private PredefinedScopeHibernateValidatorFactory getValidatorFactory() { + return Validation.byProvider( PredefinedScopeHibernateValidator.class ) + .configure() + .builtinConstraints( new HashSet<>( Arrays.asList( NotNull.class.getName() ) ) ) + .initializeBeanMetaData( new HashSet<>( Arrays.asList( + A.class, B.class, C.class, D.class, E.class, F.class, G.class + ) ) ) + .buildValidatorFactory().unwrap( PredefinedScopeHibernateValidatorFactory.class ); + } + + private ProcessedBeansTrackingStrategy getProcessedBeansTrackingStrategy() { + return ( (PredefinedScopeValidatorFactoryImpl) getValidatorFactory() ).getBeanMetaDataManager().getProcessedBeansTrackingStrategy(); + } + + private static class A { + + private String description; + + private List<@Valid B> bValues = new ArrayList<>(); + + private List<@Valid E> eValues = new ArrayList<>(); + } + + private static class B { + @Valid + private String description; + + private List<@Valid C> cValues = new ArrayList<>(); + } + + private static class C { + + private String description; + + private List<@Valid D> dValues = new ArrayList<>(); + + private List<@Valid F> fValues = new ArrayList<>(); + } + + private static class D { + + private String description; + + private List<@Valid B> bValues = new ArrayList<>(); + } + + private static class E { + + private String description; + + private List<@Valid F> fValues = new ArrayList<>(); + + private List<@Valid G> gValues = new ArrayList<>(); + + private List<@Valid G> gAnotherValues = new ArrayList<>(); + } + + private static class F { + + private String description; + + private List<@Valid G> gValues = new ArrayList<>(); + } + + private static class G { + + private String description; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesListTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesListTest.java new file mode 100644 index 0000000000..f7cd8159a4 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesListTest.java @@ -0,0 +1,196 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.PredefinedScopeHibernateValidator; +import org.hibernate.validator.PredefinedScopeHibernateValidatorFactory; +import org.hibernate.validator.internal.engine.PredefinedScopeValidatorFactoryImpl; +import org.hibernate.validator.internal.engine.tracking.ProcessedBeansTrackingStrategy; + +import org.testng.annotations.Test; + +/** + * An example of beans with cascading constraints, some cycle and others do not. + * + * A -> B ---> C ------> F -> G <- + * | ^ | ^ ^ | + * | | | | | | + * | -- D <-- | | | + * --------------------> E ------- + * + * A, B, C, D, E, F, and G are beans that get validated. + * + * An arrow, ->, indicates a cascading constraint. + * + * The following are the properties with cascading List constraints: + * A.bValues + * .eValues + * B.cValues + * C.dValues + * .fValues + * D.bValues + * E.fValues + * .gValues + * .gAnotherValues + * F.gValues + * + * @author Gail Badner + * + */ + +public class ProcessedBeansTrackingCyclesNoCyclesListTest { + + @Test + public void testTrackingEnabled() { + final ProcessedBeansTrackingStrategy processedBeansTrackingStrategy = getProcessedBeansTrackingStrategy(); + + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + A.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + B.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + C.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + D.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + E.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + F.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + G.class, + false + ) ); + } + + @Test + public void testValidate() { + final A a = new A(); + final B b = new B(); + final C c = new C(); + final D d = new D(); + final E e = new E(); + final F f = new F(); + final G g = new G(); + + a.bValues.add( b ); + a.eValues.add( e ); + b.cValues.add( c ); + c.dValues.add( d ); + d.bValues.add( b ); + e.fValues.add( f ); + e.gValues.add( g ); + e.gAnotherValues.add( g ); + f.gValues.add( g ); + + final Validator validator = getValidator(); + final Set> violationsA = validator.validate( a ); + final Set> violationsB = validator.validate( b ); + final Set> violationsC = validator.validate( c ); + final Set> violationsD = validator.validate( d ); + final Set> violationsE = validator.validate( e ); + final Set> violationsF = validator.validate( f ); + final Set> violationsG = validator.validate( g ); + } + + private Validator getValidator() { + return getValidatorFactory().getValidator(); + } + + private PredefinedScopeHibernateValidatorFactory getValidatorFactory() { + return Validation.byProvider( PredefinedScopeHibernateValidator.class ) + .configure() + .builtinConstraints( new HashSet<>( Arrays.asList( NotNull.class.getName() ) ) ) + .initializeBeanMetaData( new HashSet<>( Arrays.asList( + A.class, B.class, C.class, D.class, E.class, F.class, G.class + ) ) ) + .buildValidatorFactory().unwrap( PredefinedScopeHibernateValidatorFactory.class ); + } + + private ProcessedBeansTrackingStrategy getProcessedBeansTrackingStrategy() { + return ( (PredefinedScopeValidatorFactoryImpl) getValidatorFactory() ).getBeanMetaDataManager().getProcessedBeansTrackingStrategy(); + } + + private static class A { + + private String description; + + private List<@Valid B> bValues = new ArrayList<>(); + + private List<@Valid E> eValues = new ArrayList<>(); + } + + private static class B { + @Valid + private String description; + + private List<@Valid C> cValues = new ArrayList<>(); + } + + private static class C { + + private String description; + + private List<@Valid D> dValues = new ArrayList<>(); + + private List<@Valid F> fValues = new ArrayList<>(); + } + + private static class D { + + private String description; + + private List<@Valid B> bValues = new ArrayList<>(); + } + + private static class E { + + private String description; + + private List<@Valid F> fValues = new ArrayList<>(); + + private List<@Valid G> gValues = new ArrayList<>(); + + private List<@Valid G> gAnotherValues = new ArrayList<>(); + } + + private static class F { + + private String description; + + private List<@Valid G> gValues = new ArrayList<>(); + } + + private static class G { + + private String description; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesMapTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesMapTest.java new file mode 100644 index 0000000000..cb9741bf16 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesMapTest.java @@ -0,0 +1,185 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.PredefinedScopeHibernateValidator; +import org.hibernate.validator.PredefinedScopeHibernateValidatorFactory; +import org.hibernate.validator.internal.engine.PredefinedScopeValidatorFactoryImpl; +import org.hibernate.validator.internal.engine.tracking.ProcessedBeansTrackingStrategy; + +import org.testng.annotations.Test; + +/** + * An example of beans with cascading constraints, some cycle and others do not. + * + * A -> B ---> C ------> F -> G <- + * | ^ | ^ ^ | + * | | | | | | + * | -- D <-- | | | + * --------------------> E ------- + * + * A, B, C, D, E, F, and G are beans that get validated. + * + * An arrow, ->, indicates a cascading constraint.ProcessedBeansTrackingCyclesNoCyclesMapTest + * + * The following are the properties with cascading Map constraints: + * A.bToEs + * B.cToCs + * C.dToFs + * D.bToBs + * E.fToGs + * .gToGs + * F.gToGs + * + * @author Gail Badner + * + */ + +public class ProcessedBeansTrackingCyclesNoCyclesMapTest { + + @Test + public void testTrackingEnabled() { + final ProcessedBeansTrackingStrategy processedBeansTrackingStrategy = getProcessedBeansTrackingStrategy(); + + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + A.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + B.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + C.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + D.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + E.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + F.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + G.class, + false + ) ); + } + + @Test + public void testValidate() { + final A a = new A(); + final B b = new B(); + final C c = new C(); + final D d = new D(); + final E e = new E(); + final F f = new F(); + final G g = new G(); + + a.bToEs.put( b, e ); + b.cToCs.put( c, c ); + c.dToFs.put( d, f ); + d.bToBs.put( b, b ); + e.fToGs.put( f, g ); + e.gToGs.put( g, g ); + + final Validator validator = getValidator(); + final Set> violationsA = validator.validate( a ); + final Set> violationsB = validator.validate( b ); + final Set> violationsC = validator.validate( c ); + final Set> violationsD = validator.validate( d ); + final Set> violationsE = validator.validate( e ); + final Set> violationsF = validator.validate( f ); + final Set> violationsG = validator.validate( g ); + } + + private Validator getValidator() { + return getValidatorFactory().getValidator(); + } + + private PredefinedScopeHibernateValidatorFactory getValidatorFactory() { + return Validation.byProvider( PredefinedScopeHibernateValidator.class ) + .configure() + .builtinConstraints( new HashSet<>( Arrays.asList( NotNull.class.getName() ) ) ) + .initializeBeanMetaData( new HashSet<>( Arrays.asList( + A.class, B.class, C.class, D.class, E.class, F.class, G.class + ) ) ) + .buildValidatorFactory().unwrap( PredefinedScopeHibernateValidatorFactory.class ); + } + + private ProcessedBeansTrackingStrategy getProcessedBeansTrackingStrategy() { + return ( (PredefinedScopeValidatorFactoryImpl) getValidatorFactory() ).getBeanMetaDataManager().getProcessedBeansTrackingStrategy(); + } + + private static class A { + + private String description; + + private Map<@Valid B, @Valid E> bToEs = new HashMap<>(); + + } + + private static class B { + @Valid + private String description; + + private Map<@Valid C, @Valid C> cToCs = new HashMap<>(); + } + + private static class C { + + private String description; + + private Map<@Valid D, @Valid F> dToFs = new HashMap<>(); + } + + private static class D { + + private String description; + + private Map<@Valid B, @Valid B> bToBs = new HashMap<>(); + } + + private static class E { + + private String description; + + private Map<@Valid F, G> fToGs = new HashMap<>(); + + private Map<@Valid G, @Valid G> gToGs = new HashMap<>(); + } + + private static class F { + + private String description; + + private Map<@Valid G, @Valid G> gToGs = new HashMap<>(); + } + + private static class G { + + private String description; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesTest.java new file mode 100644 index 0000000000..92e3a84cca --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingCyclesNoCyclesTest.java @@ -0,0 +1,203 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.PredefinedScopeHibernateValidator; +import org.hibernate.validator.PredefinedScopeHibernateValidatorFactory; +import org.hibernate.validator.internal.engine.PredefinedScopeValidatorFactoryImpl; +import org.hibernate.validator.internal.engine.tracking.ProcessedBeansTrackingStrategy; + +import org.testng.annotations.Test; + +/** + * An example of beans with cascading constraints, some cycle and others do not. + * + * A -> B ---> C ------> F -> G <- + * | ^ | ^ ^ | + * | | | | | | + * | -- D <-- | | | + * --------------------> E ------- + * + * A, B, C, D, E, F, and G are beans that get validated. + * + * An arrow, ->, indicates a cascading constraint. + * + * The following are the properties with cascading constraints: + * A.b + * .e + * B.c + * C.d + * .f + * D.b + * E.f + * .g + * F.g + * + * @author Gail Badner + * + */ + +public class ProcessedBeansTrackingCyclesNoCyclesTest { + + @Test + public void testTrackingEnabled() { + final ProcessedBeansTrackingStrategy processedBeansTrackingStrategy = getProcessedBeansTrackingStrategy(); + + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + A.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + B.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + C.class, + true + ) ); + assertTrue( processedBeansTrackingStrategy.isEnabledForBean( + D.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + E.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + F.class, + true + ) ); + assertFalse( processedBeansTrackingStrategy.isEnabledForBean( + G.class, + false + ) ); + } + + @Test + public void testValidate() { + final A a = new A(); + final B b = new B(); + final C c = new C(); + final D d = new D(); + final E e = new E(); + final F f = new F(); + final G g = new G(); + + a.b = b; + a.e = e; + b.c = c; + c.d = d; + d.b = b; + e.f = f; + e.g = g; + e.gAnother = g; + f.g = g; + + final Validator validator = getValidator(); + final Set> violationsA = validator.validate( a ); + final Set> violationsB = validator.validate( b ); + final Set> violationsC = validator.validate( c ); + final Set> violationsD = validator.validate( d ); + final Set> violationsE = validator.validate( e ); + final Set> violationsF = validator.validate( f ); + final Set> violationsG = validator.validate( g ); + } + + private Validator getValidator() { + return getValidatorFactory().getValidator(); + } + + private PredefinedScopeHibernateValidatorFactory getValidatorFactory() { + return Validation.byProvider( PredefinedScopeHibernateValidator.class ) + .configure() + .builtinConstraints( new HashSet<>( Arrays.asList( NotNull.class.getName() ) ) ) + .initializeBeanMetaData( new HashSet<>( Arrays.asList( + A.class, B.class, C.class, D.class, E.class, F.class, G.class + ) ) ) + .buildValidatorFactory().unwrap( PredefinedScopeHibernateValidatorFactory.class ); + } + + private ProcessedBeansTrackingStrategy getProcessedBeansTrackingStrategy() { + return ( (PredefinedScopeValidatorFactoryImpl) getValidatorFactory() ).getBeanMetaDataManager().getProcessedBeansTrackingStrategy(); + } + + private static class A { + + private String description; + + @Valid + private B b; + + @Valid + private E e; + } + + private static class B { + @Valid + private String description; + + @Valid + private C c; + } + + private static class C { + + private String description; + + @Valid + private D d; + + @Valid + private F f; + } + + private static class D { + + private String description; + + @Valid + private B b; + } + + private static class E { + + private String description; + + @Valid + private F f; + + @Valid + private G g; + + @Valid + private G gAnother; + } + + private static class F { + + private String description; + + @Valid + private G g; + } + + private static class G { + + private String description; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingNoCycles1Test.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingNoCycles1Test.java new file mode 100644 index 0000000000..0ee2e90206 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingNoCycles1Test.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.testutils.ValidatorUtil; + +import org.testng.annotations.Test; + +/** + * This is not a real test, just an illustration. + * + * @author Guillaume Smet + */ +public class ProcessedBeansTrackingNoCycles1Test { + + @Test + public void testSerializeHibernateEmail() throws Exception { + Validator validator = ValidatorUtil.getValidator(); + + validator.validate( new Parent() ); + } + + private static class Parent { + + @NotNull + private String property; + + @Valid + private Child child; + } + + private static class Child { + + @NotNull + private String property; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingNoCycles2Test.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingNoCycles2Test.java new file mode 100644 index 0000000000..999ee71edc --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingNoCycles2Test.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.testutils.ValidatorUtil; + +import org.testng.annotations.Test; + +/** + * This is not a real test, just an illustration. + * + * @author Guillaume Smet + */ +public class ProcessedBeansTrackingNoCycles2Test { + + @Test + public void testSerializeHibernateEmail() throws Exception { + Validator validator = ValidatorUtil.getValidator(); + + final Parent parent = new Parent(); + parent.property = "parent property"; + final Child child = new Child(); + child.property = "child property"; + parent.children = new ArrayList<>(); + parent.children.add( child ); + parent.children.add( child ); + validator.validate( parent ); + } + + private static class Parent { + + @NotNull + private String property; + + private List<@Valid Child> children; + } + + private static class Child { + + @NotNull + private String property; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingNoCycles3Test.java b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingNoCycles3Test.java new file mode 100644 index 0000000000..243a576997 --- /dev/null +++ b/engine/src/test/java/org/hibernate/validator/test/internal/engine/tracking/ProcessedBeansTrackingNoCycles3Test.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.validator.test.internal.engine.tracking; + +import java.util.List; + +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; + +import org.hibernate.validator.testutils.ValidatorUtil; + +import org.testng.annotations.Test; + +/** + * This is not a real test, just an illustration. + *

+ * In this case, given we will have all the subclasses of Child in the metadata, we should be able to know there are no + * cycles. + * + * @author Guillaume Smet + */ +public class ProcessedBeansTrackingNoCycles3Test { + + @Test + public void testSerializeHibernateEmail() throws Exception { + Validator validator = ValidatorUtil.getValidator(); + + validator.validate( new Parent() ); + } + + private static class Parent { + + @NotNull + private String property; + + private List<@Valid ? extends Child> children; + } + + private static class Child { + + @NotNull + private String property; + } +} diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/metadata/BeanMetaDataManagerTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/metadata/BeanMetaDataManagerTest.java index ae7d99c151..77849faad6 100644 --- a/engine/src/test/java/org/hibernate/validator/test/internal/metadata/BeanMetaDataManagerTest.java +++ b/engine/src/test/java/org/hibernate/validator/test/internal/metadata/BeanMetaDataManagerTest.java @@ -23,6 +23,7 @@ import org.hibernate.validator.internal.engine.DefaultPropertyNodeNameProvider; import org.hibernate.validator.internal.engine.MethodValidationConfiguration; import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator; +import org.hibernate.validator.internal.engine.tracking.DefaultProcessedBeansTrackingVoter; import org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl; import org.hibernate.validator.internal.metadata.DefaultBeanMetaDataClassNormalizer; import org.hibernate.validator.internal.metadata.aggregated.BeanMetaData; @@ -60,7 +61,8 @@ public void setUpBeanMetaDataManager() { new DefaultBeanMetaDataClassNormalizer(), new ValidationOrderGenerator(), Collections.emptyList(), - new MethodValidationConfiguration.Builder().build() + new MethodValidationConfiguration.Builder().build(), + new DefaultProcessedBeansTrackingVoter() ); } diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/ExecutableMetaDataTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/ExecutableMetaDataTest.java index d5f5237fa6..d182960f34 100644 --- a/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/ExecutableMetaDataTest.java +++ b/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/ExecutableMetaDataTest.java @@ -25,6 +25,7 @@ import org.hibernate.validator.internal.engine.DefaultPropertyNodeNameProvider; import org.hibernate.validator.internal.engine.MethodValidationConfiguration; import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator; +import org.hibernate.validator.internal.engine.tracking.DefaultProcessedBeansTrackingVoter; import org.hibernate.validator.internal.metadata.BeanMetaDataManager; import org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl; import org.hibernate.validator.internal.metadata.DefaultBeanMetaDataClassNormalizer; @@ -67,7 +68,8 @@ public void setupBeanMetaData() { new DefaultBeanMetaDataClassNormalizer(), new ValidationOrderGenerator(), Collections.emptyList(), - new MethodValidationConfiguration.Builder().build() + new MethodValidationConfiguration.Builder().build(), + new DefaultProcessedBeansTrackingVoter() ); beanMetaData = beanMetaDataManager.getBeanMetaData( CustomerRepositoryExt.class ); diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/ParameterMetaDataTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/ParameterMetaDataTest.java index 8b374e66a7..9baec4461b 100644 --- a/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/ParameterMetaDataTest.java +++ b/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/ParameterMetaDataTest.java @@ -26,6 +26,7 @@ import org.hibernate.validator.internal.engine.DefaultPropertyNodeNameProvider; import org.hibernate.validator.internal.engine.MethodValidationConfiguration; import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator; +import org.hibernate.validator.internal.engine.tracking.DefaultProcessedBeansTrackingVoter; import org.hibernate.validator.internal.metadata.BeanMetaDataManager; import org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl; import org.hibernate.validator.internal.metadata.DefaultBeanMetaDataClassNormalizer; @@ -66,7 +67,8 @@ public void setupBeanMetaData() { new DefaultBeanMetaDataClassNormalizer(), new ValidationOrderGenerator(), Collections.emptyList(), - new MethodValidationConfiguration.Builder().build() + new MethodValidationConfiguration.Builder().build(), + new DefaultProcessedBeansTrackingVoter() ); beanMetaData = beanMetaDataManager.getBeanMetaData( CustomerRepository.class ); @@ -137,7 +139,8 @@ public void parameterNameInInheritanceHierarchy() throws Exception { new DefaultBeanMetaDataClassNormalizer(), new ValidationOrderGenerator(), Collections.emptyList(), - new MethodValidationConfiguration.Builder().build() + new MethodValidationConfiguration.Builder().build(), + new DefaultProcessedBeansTrackingVoter() ); BeanMetaData localBeanMetaData = beanMetaDataManager.getBeanMetaData( ServiceImpl.class ); diff --git a/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/PropertyMetaDataTest.java b/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/PropertyMetaDataTest.java index 4938cb3837..3a968a689b 100644 --- a/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/PropertyMetaDataTest.java +++ b/engine/src/test/java/org/hibernate/validator/test/internal/metadata/aggregated/PropertyMetaDataTest.java @@ -19,6 +19,7 @@ import org.hibernate.validator.internal.engine.DefaultPropertyNodeNameProvider; import org.hibernate.validator.internal.engine.MethodValidationConfiguration; import org.hibernate.validator.internal.engine.groups.ValidationOrderGenerator; +import org.hibernate.validator.internal.engine.tracking.DefaultProcessedBeansTrackingVoter; import org.hibernate.validator.internal.metadata.BeanMetaDataManager; import org.hibernate.validator.internal.metadata.BeanMetaDataManagerImpl; import org.hibernate.validator.internal.metadata.DefaultBeanMetaDataClassNormalizer; @@ -50,7 +51,8 @@ public void setupBeanMetaDataManager() { new DefaultBeanMetaDataClassNormalizer(), new ValidationOrderGenerator(), Collections.emptyList(), - new MethodValidationConfiguration.Builder().build() + new MethodValidationConfiguration.Builder().build(), + new DefaultProcessedBeansTrackingVoter() ); } diff --git a/performance/README.md b/performance/README.md index 0b0dc0775e..5eb1b5a3ab 100644 --- a/performance/README.md +++ b/performance/README.md @@ -11,8 +11,11 @@ To allow performance testing of different Hibernate Validator versions there are Choosing a profile executes the tests against the specified Hibernate Validator or BVal version, respectively. The defined profiles are: -* hv-current (Hibernate Validator 6.1.0-SNAPSHOT) -* hv-6.0 (Hibernate Validator 6.0.15.Final) +* hv-current (Hibernate Validator 9.0.0-SNAPSHOT) +* hv-8.0 (Hibernate Validator 8.0.2.Final) +* hv-6.2 (Hibernate Validator 6.2.5.Final) +* hv-6.1 (Hibernate Validator 6.1.7.Final) +* hv-6.0 (Hibernate Validator 6.0.22.Final) * hv-5.4 (Hibernate Validator 5.4.3.Final) * hv-5.3 (Hibernate Validator 5.3.4.Final) * hv-5.2 (Hibernate Validator 5.2.4.Final) @@ -64,7 +67,7 @@ If you want to run one of those profilers - pass it as parameter when running a To run a specific benchmark: - java -jar target/hibernate-validator-performance.jar CascadedValidation + java -jar target/hibernate-validator-performance-hv-current.jar CascadedValidation #### Creating reports for all major Hibernate Validator versions diff --git a/performance/pom.xml b/performance/pom.xml index 9588bc43de..08919c2a12 100644 --- a/performance/pom.xml +++ b/performance/pom.xml @@ -32,6 +32,7 @@ --> ${project.basedir}/src/main/java ${project.basedir}/src/main/java + ${project.basedir}/src/main/java