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.
014    package org.apache.tapestry5.internal.beanvalidator;
015    
016    import org.apache.tapestry5.Field;
017    import org.apache.tapestry5.FieldValidator;
018    import org.apache.tapestry5.MarkupWriter;
019    import org.apache.tapestry5.ValidationException;
020    import org.apache.tapestry5.beanvalidator.BeanValidatorGroupSource;
021    import org.apache.tapestry5.beanvalidator.ClientConstraintDescriptor;
022    import org.apache.tapestry5.beanvalidator.ClientConstraintDescriptorSource;
023    import org.apache.tapestry5.internal.BeanValidationContext;
024    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
025    import org.apache.tapestry5.json.JSONObject;
026    import org.apache.tapestry5.services.Environment;
027    import org.apache.tapestry5.services.FormSupport;
028    
029    import javax.validation.ConstraintViolation;
030    import javax.validation.MessageInterpolator;
031    import javax.validation.MessageInterpolator.Context;
032    import javax.validation.Validator;
033    import javax.validation.ValidatorFactory;
034    import javax.validation.metadata.BeanDescriptor;
035    import javax.validation.metadata.ConstraintDescriptor;
036    import javax.validation.metadata.PropertyDescriptor;
037    
038    import java.lang.annotation.Annotation;
039    import java.util.Iterator;
040    import java.util.Map;
041    import java.util.Set;
042    
043    import static java.lang.String.format;
044    
045    
046    public class BeanFieldValidator implements FieldValidator
047    {
048        private final Field field;
049        private final ValidatorFactory validatorFactory;
050        private final BeanValidatorGroupSource beanValidationGroupSource;
051        private final ClientConstraintDescriptorSource clientValidatorSource;
052        private final FormSupport formSupport;
053        private final Environment environment;
054    
055        public BeanFieldValidator(Field field,
056                                  ValidatorFactory validatorFactory,
057                                  BeanValidatorGroupSource beanValidationGroupSource,
058                                  ClientConstraintDescriptorSource clientValidatorSource,
059                                  FormSupport formSupport,
060                                  Environment environment)
061        {
062            this.field = field;
063            this.validatorFactory = validatorFactory;
064            this.beanValidationGroupSource = beanValidationGroupSource;
065            this.clientValidatorSource = clientValidatorSource;
066            this.formSupport = formSupport;
067            this.environment = environment;
068        }
069    
070        public boolean isRequired()
071        {
072            return false;
073        }
074    
075        public void render(final MarkupWriter writer)
076        {
077            final BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
078    
079            if (beanValidationContext == null)
080            {
081                return;
082            }
083    
084            final Validator validator = validatorFactory.getValidator();
085    
086            BeanDescriptor beanDescriptor = validator.getConstraintsForClass(beanValidationContext.getBeanType());
087    
088            String currentProperty = beanValidationContext.getCurrentProperty();
089    
090            if (currentProperty == null) return;
091    
092            PropertyDescriptor propertyDescriptor = beanDescriptor.getConstraintsForProperty(currentProperty);
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                JSONObject specs = new JSONObject();
109    
110                Map<String, Object> attributes = CollectionFactory.newMap();
111    
112                for (String attribute : clientConstraintDescriptor.getAttributes())
113                {
114                    Object object = descriptor.getAttributes().get(attribute);
115    
116                    if (object == null)
117                    {
118                        throw new NullPointerException(
119                                String.format("Attribute '%s' of %s is null but is required to apply client-side validation.",
120                                        attribute, descriptor));
121                    }
122                    attributes.put(attribute, object);
123                
124                    specs.put(attribute, object);
125                    
126                }
127                
128                formSupport.addValidation(field, clientConstraintDescriptor.getValidatorName(), message, specs);
129    
130            }
131            
132        }
133    
134        @SuppressWarnings("unchecked")
135        public void validate(final Object value) throws ValidationException
136        {
137    
138            final BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class);
139    
140            if (beanValidationContext == null)
141            {
142                return;
143            }
144    
145            final Validator validator = validatorFactory.getValidator();
146    
147            String currentProperty = beanValidationContext.getCurrentProperty();
148    
149            if (currentProperty == null) return;
150            
151            Class<?> beanType = beanValidationContext.getBeanType();
152            String[] path = currentProperty.split("\\.");
153            BeanDescriptor beanDescriptor = validator.getConstraintsForClass(beanType);
154            
155            for (int i = 1; i < path.length - 1; i++) 
156            {
157                Class<?> constrainedPropertyClass = getConstrainedPropertyClass(beanDescriptor, path[i]);
158                if (constrainedPropertyClass != null) {
159                    beanType = constrainedPropertyClass;
160                    beanDescriptor = validator.getConstraintsForClass(beanType);
161                }
162            }
163    
164            final String propertyName = path[path.length - 1];
165            PropertyDescriptor propertyDescriptor = beanDescriptor.getConstraintsForProperty(propertyName);
166    
167            if (propertyDescriptor == null) return;
168    
169            final Set<ConstraintViolation<Object>> violations = validator.validateValue(
170                    (Class<Object>) beanType, propertyName,
171                    value, beanValidationGroupSource.get());
172    
173            if (violations.isEmpty())
174            {
175                return;
176            }
177    
178            final StringBuilder builder = new StringBuilder();
179    
180            for (Iterator<ConstraintViolation<Object>> iterator = violations.iterator(); iterator.hasNext(); )
181            {
182                ConstraintViolation<?> violation = iterator.next();
183    
184                builder.append(format("%s %s", field.getLabel(), violation.getMessage()));
185    
186                if (iterator.hasNext())
187                    builder.append(", ");
188    
189            }
190    
191            throw new ValidationException(builder.toString());
192    
193        }
194    
195        /**
196         * Returns the class of a given property, but only if it is a constrained property of the
197         * parent class. Otherwise, it returns null.
198         */
199        final private static Class<?> getConstrainedPropertyClass(BeanDescriptor beanDescriptor, String propertyName)
200        {
201            Class<?> clasz = null;
202            for (PropertyDescriptor descriptor : beanDescriptor.getConstrainedProperties()) 
203            {
204                if (descriptor.getPropertyName().equals(propertyName)) 
205                {
206                    clasz = descriptor.getElementClass();
207                    break;
208                }
209            }
210            return clasz;
211        }
212    
213        private String interpolateMessage(final ConstraintDescriptor<?> descriptor)
214        {
215            String messageTemplate = (String) descriptor.getAttributes().get("message");
216    
217            MessageInterpolator messageInterpolator = validatorFactory.getMessageInterpolator();
218    
219            return messageInterpolator.interpolate(messageTemplate, new Context()
220            {
221    
222                public ConstraintDescriptor<?> getConstraintDescriptor()
223                {
224                    return descriptor;
225                }
226    
227                public Object getValidatedValue()
228                {
229                    return null;
230                }
231            });
232        }
233    }