    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    }