001// Copyright 2010, 2012 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007//     http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014package org.apache.tapestry5.internal.beanvalidator;
015
016import org.apache.tapestry5.Field;
017import org.apache.tapestry5.FieldValidator;
018import org.apache.tapestry5.MarkupWriter;
019import org.apache.tapestry5.ValidationException;
020import org.apache.tapestry5.beanvalidator.BeanValidatorGroupSource;
021import org.apache.tapestry5.beanvalidator.ClientConstraintDescriptor;
022import org.apache.tapestry5.beanvalidator.ClientConstraintDescriptorSource;
023import org.apache.tapestry5.commons.util.CollectionFactory;
024import org.apache.tapestry5.internal.BeanValidationContext;
025import org.apache.tapestry5.services.Environment;
026import org.apache.tapestry5.services.FormSupport;
027
028import javax.validation.ConstraintViolation;
029import javax.validation.MessageInterpolator;
030import javax.validation.MessageInterpolator.Context;
031import javax.validation.Validator;
032import javax.validation.ValidatorFactory;
033import javax.validation.metadata.BeanDescriptor;
034import javax.validation.metadata.ConstraintDescriptor;
035import javax.validation.metadata.PropertyDescriptor;
036
037import java.lang.annotation.Annotation;
038import java.util.Iterator;
039import java.util.Map;
040import java.util.Set;
041
042import static java.lang.String.format;
043
044
045public class BeanFieldValidator implements FieldValidator
046{
047    private final Field field;
048    private final ValidatorFactory validatorFactory;
049    private final BeanValidatorGroupSource beanValidationGroupSource;
050    private final ClientConstraintDescriptorSource clientValidatorSource;
051    private final FormSupport formSupport;
052    private final Environment environment;
053
054    public BeanFieldValidator(Field field,
055                              ValidatorFactory validatorFactory,
056                              BeanValidatorGroupSource beanValidationGroupSource,
057                              ClientConstraintDescriptorSource clientValidatorSource,
058                              FormSupport formSupport,
059                              Environment environment)
060    {
061        this.field = field;
062        this.validatorFactory = validatorFactory;
063        this.beanValidationGroupSource = beanValidationGroupSource;
064        this.clientValidatorSource = clientValidatorSource;
065        this.formSupport = formSupport;
066        this.environment = environment;
067    }
068
069    @Override
070    public boolean isRequired()
071    {
072        return false;
073    }
074
075    @Override
076    public void render(final MarkupWriter writer)
077    {
078        final BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
079
080        if (beanValidationContext == null)
081        {
082            return;
083        }
084
085        final Validator validator = validatorFactory.getValidator();
086
087        final String currentProperty = beanValidationContext.getCurrentProperty();
088
089        if (currentProperty == null) return;
090        
091        final ValidationInfo validationInfo = getValidationInfo(beanValidationContext, currentProperty, validator);
092        final PropertyDescriptor propertyDescriptor = validationInfo.getPropertyDescriptor();
093
094        if (propertyDescriptor == null) return;
095
096        for (final ConstraintDescriptor<?> descriptor : propertyDescriptor.getConstraintDescriptors())
097        {
098            Class<? extends Annotation> annotationType = descriptor.getAnnotation().annotationType();
099
100            ClientConstraintDescriptor clientConstraintDescriptor = clientValidatorSource.getConstraintDescriptor(annotationType);
101
102            if (clientConstraintDescriptor == null)
103            {
104                continue;
105            }
106
107            String message = format("%s %s", field.getLabel(), interpolateMessage(descriptor));
108
109            Map<String, Object> attributes = CollectionFactory.newMap();
110
111            for (String attribute : clientConstraintDescriptor.getAttributes())
112            {
113                Object object = descriptor.getAttributes().get(attribute);
114
115                if (object == null)
116                {
117                    throw new NullPointerException(
118                            String.format("Attribute '%s' of %s is null but is required to apply client-side validation.",
119                                    attribute, descriptor));
120                }
121                attributes.put(attribute, object);
122            }
123
124            clientConstraintDescriptor.applyClientValidation(writer, message, attributes);
125        }
126    }
127
128    @Override
129    @SuppressWarnings("unchecked")
130    public void validate(final Object value) throws ValidationException
131    {
132
133        final BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
134
135        if (beanValidationContext == null)
136        {
137            return;
138        }
139
140        final Validator validator = validatorFactory.getValidator();
141
142        String currentProperty = beanValidationContext.getCurrentProperty();
143
144        if (currentProperty == null) return;
145        
146        final ValidationInfo validationInfo = getValidationInfo(beanValidationContext, currentProperty, validator);
147        final PropertyDescriptor propertyDescriptor = validationInfo.getPropertyDescriptor();
148
149        if (propertyDescriptor == null) return;
150
151        final Set<ConstraintViolation<Object>> violations = validator.validateValue(
152                (Class<Object>) validationInfo.getBeanType(), validationInfo.getPropertyName(),
153                value, beanValidationGroupSource.get());
154
155        if (violations.isEmpty())
156        {
157            return;
158        }
159
160        final StringBuilder builder = new StringBuilder();
161
162        for (Iterator<ConstraintViolation<Object>> iterator = violations.iterator(); iterator.hasNext(); )
163        {
164            ConstraintViolation<?> violation = iterator.next();
165
166            builder.append(format("%s %s", field.getLabel(), violation.getMessage()));
167
168            if (iterator.hasNext())
169                builder.append(", ");
170
171        }
172
173        throw new ValidationException(builder.toString());
174
175    }
176
177    /**
178     * Returns the class of a given property, but only if it is a constrained property of the
179     * parent class. Otherwise, it returns null.
180     */
181    final private static Class<?> getConstrainedPropertyClass(BeanDescriptor beanDescriptor, String propertyName)
182    {
183        Class<?> clasz = null;
184        for (PropertyDescriptor descriptor : beanDescriptor.getConstrainedProperties()) 
185        {
186            if (descriptor.getPropertyName().equals(propertyName)) 
187            {
188                clasz = descriptor.getElementClass();
189                break;
190            }
191        }
192        return clasz;
193    }
194
195    private String interpolateMessage(final ConstraintDescriptor<?> descriptor)
196    {
197        String messageTemplate = (String) descriptor.getAttributes().get("message");
198
199        MessageInterpolator messageInterpolator = validatorFactory.getMessageInterpolator();
200
201        return messageInterpolator.interpolate(messageTemplate, new Context()
202        {
203
204            @Override
205            public ConstraintDescriptor<?> getConstraintDescriptor()
206            {
207                return descriptor;
208            }
209
210            @Override
211            public Object getValidatedValue()
212            {
213                return null;
214            }
215        });
216    }
217    
218    final private static ValidationInfo getValidationInfo(BeanValidationContext beanValidationContext, String currentProperty, Validator validator) {
219        Class<?> beanType = beanValidationContext.getBeanType();
220        String[] path = currentProperty.split("\\.");
221        BeanDescriptor beanDescriptor = validator.getConstraintsForClass(beanType);
222        
223        for (int i = 1; i < path.length - 1; i++) 
224        {
225            Class<?> constrainedPropertyClass = getConstrainedPropertyClass(beanDescriptor, path[i]);
226            if (constrainedPropertyClass != null) {
227                beanType = constrainedPropertyClass;
228                beanDescriptor = validator.getConstraintsForClass(beanType);
229            }
230        }
231
232        final String propertyName = path[path.length - 1];
233        PropertyDescriptor propertyDescriptor = beanDescriptor.getConstraintsForProperty(propertyName);
234        return new ValidationInfo(beanType, propertyName, propertyDescriptor);
235    }
236    
237    final private static class ValidationInfo {
238        final private Class<?> beanType;
239        final private String propertyName;
240        final private PropertyDescriptor propertyDescriptor;
241        public ValidationInfo(Class<?> beanType, String propertyName,
242                PropertyDescriptor propertyDescriptor) 
243        {
244            super();
245            this.beanType = beanType;
246            this.propertyName = propertyName;
247            this.propertyDescriptor = propertyDescriptor;
248        }
249        
250        public Class<?> getBeanType() 
251        {
252            return beanType;
253        }
254        
255        public String getPropertyName() 
256        {
257            return propertyName;
258        }
259
260        public PropertyDescriptor getPropertyDescriptor() 
261        {
262            return propertyDescriptor;
263        }
264
265    }
266    
267}