    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}