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.engine.constraintvalidation;
8
9 import static org.hibernate.validator.constraints.CompositionType.ALL_FALSE;
10 import static org.hibernate.validator.constraints.CompositionType.AND;
11 import static org.hibernate.validator.constraints.CompositionType.OR;
12
13 import java.lang.annotation.Annotation;
14 import java.lang.invoke.MethodHandles;
15 import java.lang.reflect.Type;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.List;
19 import java.util.Optional;
20 import java.util.stream.Collectors;
21
22 import javax.validation.ConstraintValidator;
23
24 import org.hibernate.validator.constraints.CompositionType;
25 import org.hibernate.validator.internal.engine.validationcontext.ValidationContext;
26 import org.hibernate.validator.internal.engine.valuecontext.ValueContext;
27 import org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl;
28 import org.hibernate.validator.internal.util.CollectionHelper;
29 import org.hibernate.validator.internal.util.logging.Log;
30 import org.hibernate.validator.internal.util.logging.LoggerFactory;
31 import org.hibernate.validator.internal.util.stereotypes.Immutable;
32
33 /**
34  * A constraint tree for composing constraints. Has children corresponding to the composed constraints.
35  *
36  * @author Hardy Ferentschik
37  * @author Federico Mancini
38  * @author Dag Hovland
39  * @author Kevin Pollet &lt;kevin.pollet@serli.com&gt; (C) 2012 SERLI
40  * @author Guillaume Smet
41  * @author Marko Bekhta
42  */

43 class ComposingConstraintTree<B extends Annotation> extends ConstraintTree<B> {
44
45     private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );
46
47     @Immutable
48     private final List<ConstraintTree<?>> children;
49
50     public ComposingConstraintTree(ConstraintValidatorManager constraintValidatorManager, ConstraintDescriptorImpl<B> descriptor, Type validatedValueType) {
51         super( constraintValidatorManager, descriptor, validatedValueType );
52         this.children = descriptor.getComposingConstraintImpls().stream()
53                 .map( desc -> createConstraintTree( constraintValidatorManager, desc ) )
54                 .collect( Collectors.collectingAndThen( Collectors.toList(), CollectionHelper::toImmutableList ) );
55     }
56
57     private <U extends Annotation> ConstraintTree<U> createConstraintTree(ConstraintValidatorManager constraintValidatorManager, ConstraintDescriptorImpl<U> composingDescriptor) {
58         if ( composingDescriptor.getComposingConstraintImpls().isEmpty() ) {
59             return new SimpleConstraintTree<>( constraintValidatorManager, composingDescriptor, getValidatedValueType() );
60         }
61         else {
62             return new ComposingConstraintTree<>( constraintValidatorManager, composingDescriptor, getValidatedValueType() );
63         }
64     }
65
66     @Override
67     protected void validateConstraints(ValidationContext<?> validationContext,
68             ValueContext<?, ?> valueContext,
69             Collection<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts) {
70         CompositionResult compositionResult = validateComposingConstraints(
71                 validationContext, valueContext, violatedConstraintValidatorContexts
72         );
73
74         Optional<ConstraintValidatorContextImpl> violatedLocalConstraintValidatorContext;
75
76         // After all children are validated the actual ConstraintValidator of the constraint itself is executed
77         if ( mainConstraintNeedsEvaluation( validationContext, violatedConstraintValidatorContexts ) ) {
78
79             if ( LOG.isTraceEnabled() ) {
80                 LOG.tracef(
81                         "Validating value %s against constraint defined by %s.",
82                         valueContext.getCurrentValidatedValue(),
83                         descriptor
84                 );
85             }
86
87             // find the right constraint validator
88             ConstraintValidator<B, ?> validator = getInitializedConstraintValidator( validationContext, valueContext );
89
90             // create a constraint validator context
91             ConstraintValidatorContextImpl constraintValidatorContext = validationContext.createConstraintValidatorContextFor(
92                     descriptor, valueContext.getPropertyPath()
93             );
94
95             // validate
96             violatedLocalConstraintValidatorContext = validateSingleConstraint(
97                     valueContext,
98                     constraintValidatorContext,
99                     validator
100             );
101
102             // We re-evaluate the boolean composition by taking into consideration also the violations
103             // from the local constraintValidator
104             if ( !violatedLocalConstraintValidatorContext.isPresent() ) {
105                 compositionResult.setAtLeastOneTrue( true );
106             }
107             else {
108                 compositionResult.setAllTrue( false );
109             }
110         }
111         else {
112             violatedLocalConstraintValidatorContext = Optional.empty();
113         }
114
115         if ( !passesCompositionTypeRequirement( violatedConstraintValidatorContexts, compositionResult ) ) {
116             prepareFinalConstraintViolations(
117                     validationContext, valueContext, violatedConstraintValidatorContexts, violatedLocalConstraintValidatorContext
118             );
119         }
120     }
121
122     private boolean mainConstraintNeedsEvaluation(ValidationContext<?> validationContext,
123             Collection<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts) {
124         // we are dealing with a composing constraint with no validator for the main constraint
125         if ( !descriptor.getComposingConstraints().isEmpty() && descriptor.getMatchingConstraintValidatorDescriptors().isEmpty() ) {
126             return false;
127         }
128
129         if ( violatedConstraintValidatorContexts.isEmpty() ) {
130             return true;
131         }
132
133         // report as single violation and there is already a violation
134         if ( descriptor.isReportAsSingleViolation() && descriptor.getCompositionType() == AND ) {
135             return false;
136         }
137
138         // explicit fail fast mode
139         if ( validationContext.isFailFastModeEnabled() ) {
140             return false;
141         }
142
143         return true;
144     }
145
146     /**
147      * Before the final constraint violations can be reported back we need to check whether we have a composing
148      * constraint whose result should be reported as single violation.
149      *
150      * @param validationContext meta data about top level validation
151      * @param valueContext meta data for currently validated value
152      * @param violatedConstraintValidatorContexts used to accumulate constraint validator contexts that cause constraint violations
153      * @param localConstraintValidatorContext an optional of constraint violations of top level constraint
154      */

155     private void prepareFinalConstraintViolations(ValidationContext<?> validationContext,
156             ValueContext<?, ?> valueContext,
157             Collection<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts,
158             Optional<ConstraintValidatorContextImpl> localConstraintValidatorContext) {
159         if ( reportAsSingleViolation() ) {
160             // We clear the current violations list anyway
161             violatedConstraintValidatorContexts.clear();
162
163             // But then we need to distinguish whether the local ConstraintValidator has reported
164             // violations or not (or if there is no local ConstraintValidator at all).
165             // If not we create a violation
166             // using the error message in the annotation declaration at top level.
167             if ( !localConstraintValidatorContext.isPresent() ) {
168                 violatedConstraintValidatorContexts.add(
169                         validationContext.createConstraintValidatorContextFor(
170                                 descriptor, valueContext.getPropertyPath()
171                         )
172                 );
173             }
174         }
175
176         // Now, if there were some violations reported by
177         // the local ConstraintValidator, they need to be added to constraintViolations.
178         // Whether we need to report them as a single constraint or just add them to the other violations
179         // from the composing constraints, has been taken care of in the previous conditional block.
180         // This takes also care of possible custom error messages created by the constraintValidator,
181         // as checked in test CustomErrorMessage.java
182         // If no violations have been reported from the local ConstraintValidator, or no such validator exists,
183         // then we just add an empty list.
184         if ( localConstraintValidatorContext.isPresent() ) {
185             violatedConstraintValidatorContexts.add( localConstraintValidatorContext.get() );
186         }
187     }
188
189     /**
190      * Validates all composing constraints recursively.
191      *
192      * @param validationContext Meta data about top level validation
193      * @param valueContext Meta data for currently validated value
194      * @param violatedConstraintValidatorContexts Used to accumulate constraint validator contexts that cause constraint violations
195      *
196      * @return Returns an instance of {@code CompositionResult} relevant for boolean composition of constraints
197      */

198     private CompositionResult validateComposingConstraints(ValidationContext<?> validationContext,
199             ValueContext<?, ?> valueContext,
200             Collection<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts) {
201         CompositionResult compositionResult = new CompositionResult( truefalse );
202         for ( ConstraintTree<?> tree : children ) {
203             List<ConstraintValidatorContextImpl> tmpConstraintValidatorContexts = new ArrayList<>( 5 );
204             tree.validateConstraints( validationContext, valueContext, tmpConstraintValidatorContexts );
205             violatedConstraintValidatorContexts.addAll( tmpConstraintValidatorContexts );
206
207             if ( tmpConstraintValidatorContexts.isEmpty() ) {
208                 compositionResult.setAtLeastOneTrue( true );
209                 // no need to further validate constraints, because at least one validation passed
210                 if ( descriptor.getCompositionType() == OR ) {
211                     break;
212                 }
213             }
214             else {
215                 compositionResult.setAllTrue( false );
216                 if ( descriptor.getCompositionType() == AND
217                         && ( validationContext.isFailFastModeEnabled() || descriptor.isReportAsSingleViolation() ) ) {
218                     break;
219                 }
220             }
221         }
222         return compositionResult;
223     }
224
225     private boolean passesCompositionTypeRequirement(Collection<?> constraintViolations, CompositionResult compositionResult) {
226         CompositionType compositionType = getDescriptor().getCompositionType();
227         boolean passedValidation = false;
228         switch ( compositionType ) {
229             case OR:
230                 passedValidation = compositionResult.isAtLeastOneTrue();
231                 break;
232             case AND:
233                 passedValidation = compositionResult.isAllTrue();
234                 break;
235             case ALL_FALSE:
236                 passedValidation = !compositionResult.isAtLeastOneTrue();
237                 break;
238         }
239         assert ( !passedValidation || !( compositionType == AND ) || constraintViolations.isEmpty() );
240         if ( passedValidation ) {
241             constraintViolations.clear();
242         }
243         return passedValidation;
244     }
245
246     /**
247      * @return {@code} true if the current constraint should be reported as single violation, {@code false otherwise}.
248      *         When using negation, we only report the single top-level violation, as
249      *         it is hard, especially for ALL_FALSE to give meaningful reports
250      */

251     private boolean reportAsSingleViolation() {
252         return getDescriptor().isReportAsSingleViolation()
253                 || getDescriptor().getCompositionType() == ALL_FALSE;
254     }
255
256     private static final class CompositionResult {
257
258         private boolean allTrue;
259         private boolean atLeastOneTrue;
260
261         CompositionResult(boolean allTrue, boolean atLeastOneTrue) {
262             this.allTrue = allTrue;
263             this.atLeastOneTrue = atLeastOneTrue;
264         }
265
266         public boolean isAllTrue() {
267             return allTrue;
268         }
269
270         public boolean isAtLeastOneTrue() {
271             return atLeastOneTrue;
272         }
273
274         public void setAllTrue(boolean allTrue) {
275             this.allTrue = allTrue;
276         }
277
278         public void setAtLeastOneTrue(boolean atLeastOneTrue) {
279             this.atLeastOneTrue = atLeastOneTrue;
280         }
281     }
282 }
283