1 /*
2  * Hibernate Validator, declare and validate application constraints
3  *
4  * License: Apache License, Version 2.0
5  * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
6  */

7 package org.hibernate.validator.internal.metadata.aggregated;
8
9 import java.lang.invoke.MethodHandles;
10 import java.lang.reflect.Type;
11 import java.util.ArrayDeque;
12 import java.util.Collections;
13 import java.util.Deque;
14 import java.util.EnumSet;
15 import java.util.HashMap;
16 import java.util.List;
17 import java.util.Map;
18 import java.util.Objects;
19 import java.util.Set;
20 import java.util.stream.Collectors;
21
22 import javax.validation.ElementKind;
23
24 import org.hibernate.validator.internal.engine.ConstraintCreationContext;
25 import org.hibernate.validator.internal.metadata.core.MetaConstraint;
26 import org.hibernate.validator.internal.metadata.core.MetaConstraints;
27 import org.hibernate.validator.internal.metadata.descriptor.PropertyDescriptorImpl;
28 import org.hibernate.validator.internal.metadata.facets.Cascadable;
29 import org.hibernate.validator.internal.metadata.location.ConstraintLocation;
30 import org.hibernate.validator.internal.metadata.location.GetterConstraintLocation;
31 import org.hibernate.validator.internal.metadata.location.TypeArgumentConstraintLocation;
32 import org.hibernate.validator.internal.metadata.raw.ConstrainedElement;
33 import org.hibernate.validator.internal.metadata.raw.ConstrainedElement.ConstrainedElementKind;
34 import org.hibernate.validator.internal.metadata.raw.ConstrainedExecutable;
35 import org.hibernate.validator.internal.metadata.raw.ConstrainedField;
36 import org.hibernate.validator.internal.properties.Getter;
37 import org.hibernate.validator.internal.properties.Property;
38 import org.hibernate.validator.internal.properties.javabean.JavaBeanGetter;
39 import org.hibernate.validator.internal.util.CollectionHelper;
40 import org.hibernate.validator.internal.util.logging.Log;
41 import org.hibernate.validator.internal.util.logging.LoggerFactory;
42 import org.hibernate.validator.internal.util.stereotypes.Immutable;
43
44 /**
45  * Represents the constraint related meta data for a JavaBeans property.
46  * Abstracts from the concrete physical type of the underlying Java element(s)
47  * (fields or getter methods).
48  * <p>
49  * In order to provide a unified access to all JavaBeans constraints also
50  * class-level constraints are represented by this meta data type.
51  * </p>
52  * <p>
53  * Identity is solely based on the property name, hence sets and similar
54  * collections of this type may only be created in the scope of one Java type.
55  * </p>
56  *
57  * @author Gunnar Morling
58  * @author Guillaume Smet
59  */

60 public class PropertyMetaData extends AbstractConstraintMetaData {
61
62     private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );
63
64     @Immutable
65     private final Set<Cascadable> cascadables;
66
67     private PropertyMetaData(String propertyName,
68                              Type type,
69                              Set<MetaConstraint<?>> constraints,
70                              Set<MetaConstraint<?>> containerElementsConstraints,
71                              Set<Cascadable> cascadables) {
72         super(
73                 propertyName,
74                 type,
75                 constraints,
76                 containerElementsConstraints,
77                 !cascadables.isEmpty(),
78                 !cascadables.isEmpty() || !constraints.isEmpty() || !containerElementsConstraints.isEmpty()
79         );
80
81         this.cascadables = CollectionHelper.toImmutableSet( cascadables );
82     }
83
84     @Override
85     public PropertyDescriptorImpl asDescriptor(boolean defaultGroupSequenceRedefined, List<Class<?>> defaultGroupSequence) {
86         // TODO we have one CascadingMetaData per Cascadable but we need only one to provide a view to the
87         // Bean Validation metadata API so we pick the first one...
88         CascadingMetaData firstCascadingMetaData = cascadables.isEmpty() ? null : cascadables.iterator().next().getCascadingMetaData();
89
90         return new PropertyDescriptorImpl(
91                 getType(),
92                 getName(),
93                 asDescriptors( getDirectConstraints() ),
94                 asContainerElementTypeDescriptors( getContainerElementsConstraints(), firstCascadingMetaData, defaultGroupSequenceRedefined, defaultGroupSequence ),
95                 firstCascadingMetaData != null ? firstCascadingMetaData.isCascading() : false,
96                 defaultGroupSequenceRedefined,
97                 defaultGroupSequence,
98                 firstCascadingMetaData != null ? firstCascadingMetaData.getGroupConversionDescriptors() : Collections.emptySet()
99         );
100     }
101
102     /**
103      * Returns the cascadables of this property, if any. Often, there will be just a single element returned. Several
104      * elements may be returned in the following cases:
105      * <ul>
106      * <li>a property's field has been marked with {@code @Valid} but type-level constraints have been given on the
107      * getter</li>
108      * <li>one type parameter of a property has been marked with {@code @Valid} on the field (e.g. a map's key) but
109      * another type parameter has been marked with {@code @Valid} on the property (e.g. the map's value)</li>
110      * <li>a (shaded) private field in a super-type and another field of the same name in a sub-type are both marked
111      * with {@code @Valid}</li>
112      * </ul>
113      */

114     public Set<Cascadable> getCascadables() {
115         return cascadables;
116     }
117
118     @Override
119     public String toString() {
120         return "PropertyMetaData [type=" + getType() + ", propertyName=" + getName() + "]]";
121     }
122
123     @Override
124     public ElementKind getKind() {
125         return ElementKind.PROPERTY;
126     }
127
128     @Override
129     public int hashCode() {
130         return super.hashCode();
131     }
132
133     @Override
134     public boolean equals(Object obj) {
135         if ( this == obj ) {
136             return true;
137         }
138         if ( !super.equals( obj ) ) {
139             return false;
140         }
141         if ( getClass() != obj.getClass() ) {
142             return false;
143         }
144         return true;
145     }
146
147     public static class Builder extends MetaDataBuilder {
148
149         private static final EnumSet<ConstrainedElementKind> SUPPORTED_ELEMENT_KINDS = EnumSet.of(
150                 ConstrainedElementKind.FIELD,
151                 ConstrainedElementKind.GETTER
152         );
153
154         private final String propertyName;
155         private final Map<Property, Cascadable.Builder> cascadableBuilders = new HashMap<>();
156         private final Type propertyType;
157
158         public Builder(Class<?> beanClass, ConstrainedField constrainedProperty, ConstraintCreationContext constraintCreationContext) {
159             super( beanClass, constraintCreationContext );
160
161             this.propertyName = constrainedProperty.getField().getName();
162             this.propertyType = constrainedProperty.getField().getType();
163             add( constrainedProperty );
164         }
165
166         public Builder(Class<?> beanClass, ConstrainedExecutable constrainedMethod, ConstraintCreationContext constraintCreationContext) {
167             super( beanClass, constraintCreationContext );
168
169             this.propertyName = constrainedMethod.getCallable().as( Property.class ).getPropertyName();
170             this.propertyType = constrainedMethod.getCallable().getType();
171             add( constrainedMethod );
172         }
173
174         @Override
175         public boolean accepts(ConstrainedElement constrainedElement) {
176             if ( !SUPPORTED_ELEMENT_KINDS.contains( constrainedElement.getKind() ) ) {
177                 return false;
178             }
179
180             return Objects.equals( getPropertyName( constrainedElement ), propertyName );
181         }
182
183         @Override
184         public final void add(ConstrainedElement constrainedElement) {
185             super.add( constrainedElement );
186
187             if ( constrainedElement.getCascadingMetaDataBuilder().isMarkedForCascadingOnAnnotatedObjectOrContainerElements() ||
188                     constrainedElement.getCascadingMetaDataBuilder().hasGroupConversionsOnAnnotatedObjectOrContainerElements() ) {
189
190                 Property property = getConstrainableFromConstrainedElement( constrainedElement );
191
192                 Cascadable.Builder builder = cascadableBuilders.get( property );
193                 if ( builder == null ) {
194                     builder = AbstractPropertyCascadable.AbstractBuilder.builder(
195                             constraintCreationContext.getValueExtractorManager(), property,
196                             constrainedElement.getCascadingMetaDataBuilder() );
197                     cascadableBuilders.put( property, builder );
198                 }
199                 else {
200                     builder.mergeCascadingMetaData( constrainedElement.getCascadingMetaDataBuilder() );
201                 }
202             }
203         }
204
205         private Property getConstrainableFromConstrainedElement(ConstrainedElement constrainedElement) {
206             switch ( constrainedElement.getKind() ) {
207                 case FIELD:
208                     if ( constrainedElement instanceof ConstrainedField ) {
209                         return ( (ConstrainedField) constrainedElement ).getField();
210                     }
211                     else {
212                         throw LOG.getUnexpectedConstraintElementType( ConstrainedField.class, constrainedElement.getClass() );
213                     }
214                 case GETTER:
215                     if ( constrainedElement instanceof ConstrainedExecutable ) {
216                         return ( (ConstrainedExecutable) constrainedElement ).getCallable().as( Getter.class );
217                     }
218                     else {
219                         throw LOG.getUnexpectedConstraintElementType( ConstrainedExecutable.class, constrainedElement.getClass() );
220                     }
221                 default:
222                     throw LOG.getUnsupportedConstraintElementType( constrainedElement.getKind() );
223             }
224         }
225
226         @Override
227         protected Set<MetaConstraint<?>> adaptConstraints(ConstrainedElement constrainedElement, Set<MetaConstraint<?>> constraints) {
228             if ( constraints.isEmpty() || constrainedElement.getKind() != ConstrainedElementKind.GETTER ) {
229                 return constraints;
230             }
231
232             ConstraintLocation getterConstraintLocation = ConstraintLocation
233                     .forGetter( ( (ConstrainedExecutable) constrainedElement ).getCallable().as( JavaBeanGetter.class ) );
234
235             // convert return value locations into getter locations for usage within this meta-data
236             return constraints.stream()
237                     .map( c -> withGetterLocation( getterConstraintLocation, c ) )
238                     .collect( Collectors.toSet() );
239         }
240
241         private MetaConstraint<?> withGetterLocation(ConstraintLocation getterConstraintLocation, MetaConstraint<?> constraint) {
242             ConstraintLocation converted = null;
243
244             // fast track if it's a regular constraint
245             if ( !( constraint.getLocation() instanceof TypeArgumentConstraintLocation ) ) {
246                 // Change the constraint location to a GetterConstraintLocation if it is not already one
247                 if ( constraint.getLocation() instanceof GetterConstraintLocation ) {
248                     converted = constraint.getLocation();
249                 }
250                 else {
251                     converted = getterConstraintLocation;
252                 }
253             }
254             else {
255                 Deque<ConstraintLocation> locationStack = new ArrayDeque<>();
256
257                 // 1. collect the hierarchy of delegates up to the root return value location
258                 ConstraintLocation current = constraint.getLocation();
259                 do {
260                     locationStack.addFirst( current );
261                     if ( current instanceof TypeArgumentConstraintLocation ) {
262                         current = ( (TypeArgumentConstraintLocation) current ).getDelegate();
263                     }
264                     else {
265                         current = null;
266                     }
267                 }
268                 while ( current != null );
269
270                 // 2. beginning at the root, transform each location so it references the transformed delegate
271                 for ( ConstraintLocation location : locationStack ) {
272                     if ( !(location instanceof TypeArgumentConstraintLocation) ) {
273                         // Change the constraint location to a GetterConstraintLocation if it is not already one
274                         if ( location instanceof GetterConstraintLocation ) {
275                             converted = location;
276                         }
277                         else {
278                             converted = getterConstraintLocation;
279                         }
280                     }
281                     else {
282                         converted = ConstraintLocation.forTypeArgument(
283                             converted,
284                             ( (TypeArgumentConstraintLocation) location ).getTypeParameter(),
285                             location.getTypeForValidatorResolution()
286                         );
287                     }
288                 }
289             }
290
291             return MetaConstraints.create( constraintCreationContext.getTypeResolutionHelper(),
292                     constraintCreationContext.getValueExtractorManager(),
293                     constraintCreationContext.getConstraintValidatorManager(), constraint.getDescriptor(), converted );
294         }
295
296         private String getPropertyName(ConstrainedElement constrainedElement) {
297             if ( constrainedElement.getKind() == ConstrainedElementKind.FIELD ) {
298                 return ( (ConstrainedField) constrainedElement ).getField().getPropertyName();
299             }
300             else if ( constrainedElement.getKind() == ConstrainedElementKind.GETTER ) {
301                 return ( (ConstrainedExecutable) constrainedElement ).getCallable().as( Property.class ).getPropertyName();
302             }
303
304             return null;
305         }
306
307         @Override
308         public PropertyMetaData build() {
309             Set<Cascadable> cascadables = cascadableBuilders.values()
310                     .stream()
311                     .map( b -> b.build() )
312                     .collect( Collectors.toSet() );
313
314             return new PropertyMetaData(
315                     propertyName,
316                     propertyType,
317                     adaptOriginsAndImplicitGroups( getDirectConstraints() ),
318                     adaptOriginsAndImplicitGroups( getContainerElementConstraints() ),
319                     cascadables
320             );
321         }
322     }
323 }
324