/*
* Hibernate Validator, declare and validate application constraints
*
* License: Apache License, Version 2.0
* See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package org.hibernate.validator.internal.metadata.aggregated;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.ElementKind;
import org.hibernate.validator.HibernateValidatorPermission;
import org.hibernate.validator.internal.engine.valueextraction.ValueExtractorManager;
import org.hibernate.validator.internal.metadata.core.ConstraintHelper;
import org.hibernate.validator.internal.metadata.core.MetaConstraint;
import org.hibernate.validator.internal.metadata.core.MetaConstraints;
import org.hibernate.validator.internal.metadata.descriptor.PropertyDescriptorImpl;
import org.hibernate.validator.internal.metadata.facets.Cascadable;
import org.hibernate.validator.internal.metadata.location.ConstraintLocation;
import org.hibernate.validator.internal.metadata.location.GetterConstraintLocation;
import org.hibernate.validator.internal.metadata.location.TypeArgumentConstraintLocation;
import org.hibernate.validator.internal.metadata.raw.ConstrainedElement;
import org.hibernate.validator.internal.metadata.raw.ConstrainedElement.ConstrainedElementKind;
import org.hibernate.validator.internal.metadata.raw.ConstrainedExecutable;
import org.hibernate.validator.internal.metadata.raw.ConstrainedField;
import org.hibernate.validator.internal.metadata.raw.ConstrainedType;
import org.hibernate.validator.internal.util.CollectionHelper;
import org.hibernate.validator.internal.util.ReflectionHelper;
import org.hibernate.validator.internal.util.TypeResolutionHelper;
import org.hibernate.validator.internal.util.privilegedactions.GetDeclaredMethod;
import org.hibernate.validator.internal.util.stereotypes.Immutable;
Represents the constraint related meta data for a JavaBeans property.
Abstracts from the concrete physical type of the underlying Java element(s)
(fields or getter methods).
In order to provide a unified access to all JavaBeans constraints also
class-level constraints are represented by this meta data type.
Identity is solely based on the property name, hence sets and similar
collections of this type may only be created in the scope of one Java type.
Author: Gunnar Morling, Guillaume Smet
/**
* Represents the constraint related meta data for a JavaBeans property.
* Abstracts from the concrete physical type of the underlying Java element(s)
* (fields or getter methods).
* <p>
* In order to provide a unified access to all JavaBeans constraints also
* class-level constraints are represented by this meta data type.
* </p>
* <p>
* Identity is solely based on the property name, hence sets and similar
* collections of this type may only be created in the scope of one Java type.
* </p>
*
* @author Gunnar Morling
* @author Guillaume Smet
*/
public class PropertyMetaData extends AbstractConstraintMetaData {
@Immutable
private final Set<Cascadable> cascadables;
private PropertyMetaData(String propertyName,
Type type,
Set<MetaConstraint<?>> constraints,
Set<MetaConstraint<?>> containerElementsConstraints,
Set<Cascadable> cascadables,
boolean cascadingProperty) {
super(
propertyName,
type,
constraints,
containerElementsConstraints,
!cascadables.isEmpty(),
!cascadables.isEmpty() || !constraints.isEmpty() || !containerElementsConstraints.isEmpty()
);
this.cascadables = CollectionHelper.toImmutableSet( cascadables );
}
@Override
public PropertyDescriptorImpl asDescriptor(boolean defaultGroupSequenceRedefined, List<Class<?>> defaultGroupSequence) {
// TODO we have one CascadingMetaData per Cascadable but we need only one to provide a view to the
// Bean Validation metadata API so we pick the first one...
CascadingMetaData firstCascadingMetaData = cascadables.isEmpty() ? null : cascadables.iterator().next().getCascadingMetaData();
return new PropertyDescriptorImpl(
getType(),
getName(),
asDescriptors( getDirectConstraints() ),
asContainerElementTypeDescriptors( getContainerElementsConstraints(), firstCascadingMetaData, defaultGroupSequenceRedefined, defaultGroupSequence ),
firstCascadingMetaData != null ? firstCascadingMetaData.isCascading() : false,
defaultGroupSequenceRedefined,
defaultGroupSequence,
firstCascadingMetaData != null ? firstCascadingMetaData.getGroupConversionDescriptors() : Collections.emptySet()
);
}
Returns the cascadables of this property, if any. Often, there will be just a single element returned. Several
elements may be returned in the following cases:
- a property's field has been marked with
@Valid
but type-level constraints have been given on the getter
- one type parameter of a property has been marked with
@Valid
on the field (e.g. a map's key) but another type parameter has been marked with @Valid
on the property (e.g. the map's value)
- a (shaded) private field in a super-type and another field of the same name in a sub-type are both marked with
@Valid
/**
* Returns the cascadables of this property, if any. Often, there will be just a single element returned. Several
* elements may be returned in the following cases:
* <ul>
* <li>a property's field has been marked with {@code @Valid} but type-level constraints have been given on the
* getter</li>
* <li>one type parameter of a property has been marked with {@code @Valid} on the field (e.g. a map's key) but
* another type parameter has been marked with {@code @Valid} on the property (e.g. the map's value)</li>
* <li>a (shaded) private field in a super-type and another field of the same name in a sub-type are both marked
* with {@code @Valid}</li>
* </ul>
*/
public Set<Cascadable> getCascadables() {
return cascadables;
}
@Override
public String toString() {
return "PropertyMetaData [type=" + getType() + ", propertyName=" + getName() + "]]";
}
@Override
public ElementKind getKind() {
return ElementKind.PROPERTY;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public boolean equals(Object obj) {
if ( this == obj ) {
return true;
}
if ( !super.equals( obj ) ) {
return false;
}
if ( getClass() != obj.getClass() ) {
return false;
}
return true;
}
public static class Builder extends MetaDataBuilder {
private static final EnumSet<ConstrainedElementKind> SUPPORTED_ELEMENT_KINDS = EnumSet.of(
ConstrainedElementKind.TYPE,
ConstrainedElementKind.FIELD,
ConstrainedElementKind.METHOD
);
private final String propertyName;
private final Map<Member, Cascadable.Builder> cascadableBuilders = new HashMap<>();
private final Type propertyType;
private boolean cascadingProperty = false;
private Method getterAccessibleMethod;
public Builder(Class<?> beanClass, ConstrainedField constrainedField, ConstraintHelper constraintHelper, TypeResolutionHelper typeResolutionHelper,
ValueExtractorManager valueExtractorManager) {
super( beanClass, constraintHelper, typeResolutionHelper, valueExtractorManager );
this.propertyName = constrainedField.getField().getName();
this.propertyType = ReflectionHelper.typeOf( constrainedField.getField() );
add( constrainedField );
}
public Builder(Class<?> beanClass, ConstrainedType constrainedType, ConstraintHelper constraintHelper, TypeResolutionHelper typeResolutionHelper,
ValueExtractorManager valueExtractorManager) {
super( beanClass, constraintHelper, typeResolutionHelper, valueExtractorManager );
this.propertyName = null;
this.propertyType = null;
add( constrainedType );
}
public Builder(Class<?> beanClass, ConstrainedExecutable constrainedMethod, ConstraintHelper constraintHelper, TypeResolutionHelper typeResolutionHelper,
ValueExtractorManager valueExtractorManager) {
super( beanClass, constraintHelper, typeResolutionHelper, valueExtractorManager );
this.propertyName = ReflectionHelper.getPropertyName( constrainedMethod.getExecutable() );
this.propertyType = ReflectionHelper.typeOf( constrainedMethod.getExecutable() );
add( constrainedMethod );
}
@Override
public boolean accepts(ConstrainedElement constrainedElement) {
if ( !SUPPORTED_ELEMENT_KINDS.contains( constrainedElement.getKind() ) ) {
return false;
}
if ( constrainedElement.getKind() == ConstrainedElementKind.METHOD &&
!( (ConstrainedExecutable) constrainedElement ).isGetterMethod() ) {
return false;
}
return Objects.equals( getPropertyName( constrainedElement ), propertyName );
}
@Override
public final void add(ConstrainedElement constrainedElement) {
// if we are in the case of a getter and if we have constraints (either on the annotated object itself or on
// a container element) or cascaded validation, we want to create an accessible version of the getter only once.
if ( constrainedElement.getKind() == ConstrainedElementKind.METHOD && constrainedElement.isConstrained() ) {
getterAccessibleMethod = getAccessible( (Method) ( (ConstrainedExecutable) constrainedElement ).getExecutable() );
}
super.add( constrainedElement );
cascadingProperty = cascadingProperty || constrainedElement.getCascadingMetaDataBuilder().isCascading();
if ( constrainedElement.getCascadingMetaDataBuilder().isMarkedForCascadingOnAnnotatedObjectOrContainerElements() ||
constrainedElement.getCascadingMetaDataBuilder().hasGroupConversionsOnAnnotatedObjectOrContainerElements() ) {
if ( constrainedElement.getKind() == ConstrainedElementKind.FIELD ) {
Field field = ( (ConstrainedField) constrainedElement ).getField();
Cascadable.Builder builder = cascadableBuilders.get( field );
if ( builder == null ) {
builder = new FieldCascadable.Builder( valueExtractorManager, field, constrainedElement.getCascadingMetaDataBuilder() );
cascadableBuilders.put( field, builder );
}
else {
builder.mergeCascadingMetaData( constrainedElement.getCascadingMetaDataBuilder() );
}
}
else if ( constrainedElement.getKind() == ConstrainedElementKind.METHOD ) {
Method method = (Method) ( (ConstrainedExecutable) constrainedElement ).getExecutable();
Cascadable.Builder builder = cascadableBuilders.get( method );
if ( builder == null ) {
builder = new GetterCascadable.Builder( valueExtractorManager, getterAccessibleMethod, constrainedElement.getCascadingMetaDataBuilder() );
cascadableBuilders.put( method, builder );
}
else {
builder.mergeCascadingMetaData( constrainedElement.getCascadingMetaDataBuilder() );
}
}
}
}
@Override
protected Set<MetaConstraint<?>> adaptConstraints(ConstrainedElement constrainedElement, Set<MetaConstraint<?>> constraints) {
if ( constraints.isEmpty() || constrainedElement.getKind() != ConstrainedElementKind.METHOD ) {
return constraints;
}
ConstraintLocation getterConstraintLocation = ConstraintLocation.forGetter( getterAccessibleMethod );
// convert return value locations into getter locations for usage within this meta-data
return constraints.stream()
.map( c -> withGetterLocation( getterConstraintLocation, c ) )
.collect( Collectors.toSet() );
}
private MetaConstraint<?> withGetterLocation(ConstraintLocation getterConstraintLocation, MetaConstraint<?> constraint) {
ConstraintLocation converted = null;
// fast track if it's a regular constraint
if ( !(constraint.getLocation() instanceof TypeArgumentConstraintLocation) ) {
// Change the constraint location to a GetterConstraintLocation if it is not already one
if ( constraint.getLocation() instanceof GetterConstraintLocation ) {
converted = constraint.getLocation();
}
else {
converted = getterConstraintLocation;
}
}
else {
Deque<ConstraintLocation> locationStack = new ArrayDeque<>();
// 1. collect the hierarchy of delegates up to the root return value location
ConstraintLocation current = constraint.getLocation();
do {
locationStack.addFirst( current );
if ( current instanceof TypeArgumentConstraintLocation ) {
current = ( (TypeArgumentConstraintLocation) current ).getDelegate();
}
else {
current = null;
}
}
while ( current != null );
// 2. beginning at the root, transform each location so it references the transformed delegate
for ( ConstraintLocation location : locationStack ) {
if ( !(location instanceof TypeArgumentConstraintLocation) ) {
// Change the constraint location to a GetterConstraintLocation if it is not already one
if ( location instanceof GetterConstraintLocation ) {
converted = location;
}
else {
converted = getterConstraintLocation;
}
}
else {
converted = ConstraintLocation.forTypeArgument(
converted,
( (TypeArgumentConstraintLocation) location ).getTypeParameter(),
location.getTypeForValidatorResolution()
);
}
}
}
return MetaConstraints.create( typeResolutionHelper, valueExtractorManager, constraint.getDescriptor(), converted );
}
private String getPropertyName(ConstrainedElement constrainedElement) {
if ( constrainedElement.getKind() == ConstrainedElementKind.FIELD ) {
return ReflectionHelper.getPropertyName( ( (ConstrainedField) constrainedElement ).getField() );
}
else if ( constrainedElement.getKind() == ConstrainedElementKind.METHOD ) {
return ReflectionHelper.getPropertyName( ( (ConstrainedExecutable) constrainedElement ).getExecutable() );
}
return null;
}
Returns an accessible copy of the given member.
/**
* Returns an accessible copy of the given member.
*/
private Method getAccessible(Method original) {
SecurityManager sm = System.getSecurityManager();
if ( sm != null ) {
sm.checkPermission( HibernateValidatorPermission.ACCESS_PRIVATE_MEMBERS );
}
Class<?> clazz = original.getDeclaringClass();
return run( GetDeclaredMethod.andMakeAccessible( clazz, original.getName() ) );
}
private <T> T run(PrivilegedAction<T> action) {
return System.getSecurityManager() != null ? AccessController.doPrivileged( action ) : action.run();
}
@Override
public PropertyMetaData build() {
Set<Cascadable> cascadables = cascadableBuilders.values()
.stream()
.map( b -> b.build() )
.collect( Collectors.toSet() );
return new PropertyMetaData(
propertyName,
propertyType,
adaptOriginsAndImplicitGroups( getDirectConstraints() ),
adaptOriginsAndImplicitGroups( getContainerElementConstraints() ),
cascadables,
cascadingProperty
);
}
}
}