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.internal.BeanValidationContext;
024import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
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    public boolean isRequired()
070    {
071        return false;
072    }
073
074    public void render(final MarkupWriter writer)
075    {
076        final BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
077
078        if (beanValidationContext == null)
079        {
080            return;
081        }
082
083        final Validator validator = validatorFactory.getValidator();
084
085        BeanDescriptor beanDescriptor = validator.getConstraintsForClass(beanValidationContext.getBeanType());
086
087        String currentProperty = beanValidationContext.getCurrentProperty();
088
089        if (currentProperty == null) return;
090
091        PropertyDescriptor propertyDescriptor = beanDescriptor.getConstraintsForProperty(currentProperty);
092
093        if (propertyDescriptor == null) return;
094
095        for (final ConstraintDescriptor<?> descriptor : propertyDescriptor.getConstraintDescriptors())
096        {
097            Class<? extends Annotation> annotationType = descriptor.getAnnotation().annotationType();
098
099            ClientConstraintDescriptor clientConstraintDescriptor = clientValidatorSource.getConstraintDescriptor(annotationType);
100
101            if (clientConstraintDescriptor == null)
102            {
103                continue;
104            }
105
106            String message = format("%s %s", field.getLabel(), interpolateMessage(descriptor));
107
108            Map<String, Object> attributes = CollectionFactory.newMap();
109
110            for (String attribute : clientConstraintDescriptor.getAttributes())
111            {
112                Object object = descriptor.getAttributes().get(attribute);
113
114                if (object == null)
115                {
116                    throw new NullPointerException(
117                            String.format("Attribute '%s' of %s is null but is required to apply client-side validation.",
118                                    attribute, descriptor));
119                }
120                attributes.put(attribute, object);
121            }
122
123            clientConstraintDescriptor.applyClientValidation(writer, message, attributes);
124        }
125    }
126
127    @SuppressWarnings("unchecked")
128    public void validate(final Object value) throws ValidationException
129    {
130
131        final BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
132
133        if (beanValidationContext == null)
134        {
135            return;
136        }
137
138        final Validator validator = validatorFactory.getValidator();
139
140        String currentProperty = beanValidationContext.getCurrentProperty();
141
142        if (currentProperty == null) return;
143        
144        Class<?> beanType = beanValidationContext.getBeanType();
145        String[] path = currentProperty.split("\\.");
146        BeanDescriptor beanDescriptor = validator.getConstraintsForClass(beanType);
147        
148        for (int i = 1; i < path.length - 1; i++) 
149        {
150            Class<?> constrainedPropertyClass = getConstrainedPropertyClass(beanDescriptor, path[i]);
151            if (constrainedPropertyClass != null) {
152                beanType = constrainedPropertyClass;
153                beanDescriptor = validator.getConstraintsForClass(beanType);
154            }
155        }
156
157        final String propertyName = path[path.length - 1];
158        PropertyDescriptor propertyDescriptor = beanDescriptor.getConstraintsForProperty(propertyName);
159
160        if (propertyDescriptor == null) return;
161
162        final Set<ConstraintViolation<Object>> violations = validator.validateValue(
163                (Class<Object>) beanType, propertyName,
164                value, beanValidationGroupSource.get());
165
166        if (violations.isEmpty())
167        {
168            return;
169        }
170
171        final StringBuilder builder = new StringBuilder();
172
173        for (Iterator<ConstraintViolation<Object>> iterator = violations.iterator(); iterator.hasNext(); )
174        {
175            ConstraintViolation<?> violation = iterator.next();
176
177            builder.append(format("%s %s", field.getLabel(), violation.getMessage()));
178
179            if (iterator.hasNext())
180                builder.append(", ");
181
182        }
183
184        throw new ValidationException(builder.toString());
185
186    }
187
188    /**
189     * Returns the class of a given property, but only if it is a constrained property of the
190     * parent class. Otherwise, it returns null.
191     */
192    final private static Class<?> getConstrainedPropertyClass(BeanDescriptor beanDescriptor, String propertyName)
193    {
194        Class<?> clasz = null;
195        for (PropertyDescriptor descriptor : beanDescriptor.getConstrainedProperties()) 
196        {
197            if (descriptor.getPropertyName().equals(propertyName)) 
198            {
199                clasz = descriptor.getElementClass();
200                break;
201            }
202        }
203        return clasz;
204    }
205
206    private String interpolateMessage(final ConstraintDescriptor<?> descriptor)
207    {
208        String messageTemplate = (String) descriptor.getAttributes().get("message");
209
210        MessageInterpolator messageInterpolator = validatorFactory.getMessageInterpolator();
211
212        return messageInterpolator.interpolate(messageTemplate, new Context()
213        {
214
215            public ConstraintDescriptor<?> getConstraintDescriptor()
216            {
217                return descriptor;
218            }
219
220            public Object getValidatedValue()
221            {
222                return null;
223            }
224        });
225    }
226}