DATAREST-798 - Fixed invalid implementation of ValidationErrors.

Changed the implementation of ValidationErrors to be based on AbstractBeanPropertyBindingResult to consider the nesting implemented in superclasses and using a PersistentPropertyAccessor to lookup the property values.

ValidatingRepositoryEventListener now uses this implementation if a PersistentEntity can be obtained for the type under consideration, falling back to a DirectFieldBindingResult otherwise.
This commit is contained in:
Oliver Gierke
2016-04-04 15:12:21 +02:00
parent 9e58ab92c5
commit b530cbe471
5 changed files with 143 additions and 65 deletions

View File

@@ -1,88 +1,100 @@
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.core;
import static org.springframework.util.ReflectionUtils.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.ConfigurablePropertyAccessor;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.validation.AbstractErrors;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.util.Assert;
import org.springframework.validation.AbstractPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
/**
* An {@link Errors} implementation for use in the events mechanism of Spring Data REST.
* An {@link Errors} implementation for use in the events mechanism of Spring Data REST. Customizes actual field lookup
* by using a {@link PersistentPropertyAccessor} for actual value lookups.
*
* @author Jon Brisbin
* @author Oliver Gierke
*/
public class ValidationErrors extends AbstractErrors {
public class ValidationErrors extends AbstractPropertyBindingResult {
private static final long serialVersionUID = 8141826537389141361L;
private String name;
private Object entity;
private PersistentEntity<?, ?> persistentEntity;
private List<ObjectError> globalErrors = new ArrayList<ObjectError>();
private List<FieldError> fieldErrors = new ArrayList<FieldError>();
private final PersistentPropertyAccessor accessor;
private PersistentEntity<?, ?> entity;
/**
* Creates a new {@link ValidationErrors} instance for the given source object and {@link PersistentEntity}.
*
* @param source the source object to gather validation errors on, must not be {@literal null}.
* @param entity the {@link PersistentEntity} for the given source instance, must not be {@literal null}.
*/
public ValidationErrors(Object source, PersistentEntity<?, ?> entity) {
super(source.getClass().getSimpleName());
Assert.notNull(source, "Entity must not be null!");
Assert.notNull(entity, "PersistentEntity must not be null!");
Assert.isTrue(entity.getType().isInstance(source),
"Given source object is not of type of the given PersistentEntity");
public ValidationErrors(String name, Object entity, PersistentEntity<?, ?> persistentEntity) {
this.name = name;
this.entity = entity;
this.persistentEntity = persistentEntity;
this.accessor = entity.getPropertyAccessor(source);
}
/*
* (non-Javadoc)
* @see org.springframework.validation.AbstractPropertyBindingResult#getPropertyAccessor()
*/
@Override
public String getObjectName() {
return name;
}
@Override
public void reject(String errorCode, Object[] errorArgs, String defaultMessage) {
globalErrors.add(new ObjectError(name, new String[] { errorCode }, errorArgs, defaultMessage));
}
@Override
public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) {
fieldErrors.add(new FieldError(name, field, getFieldValue(field), true, new String[] { errorCode }, errorArgs,
defaultMessage));
}
@Override
public void addAllErrors(Errors errors) {
globalErrors.addAll(errors.getAllErrors());
}
@Override
public List<ObjectError> getGlobalErrors() {
return globalErrors;
}
@Override
public List<FieldError> getFieldErrors() {
return fieldErrors;
public ConfigurablePropertyAccessor getPropertyAccessor() {
return new DirectFieldAccessor(getTarget()) {
@Override
public Object getPropertyValue(String propertyName) throws BeansException {
PersistentProperty<?> property = entity.getPersistentProperty(propertyName);
return property == null ? null : accessor.getProperty(property);
}
};
}
/*
* (non-Javadoc)
* @see org.springframework.validation.AbstractBindingResult#getFieldValue(java.lang.String)
*/
@Override
public Object getFieldValue(String field) {
PersistentProperty<?> prop = persistentEntity != null ? persistentEntity.getPersistentProperty(field) : null;
if (null == prop) {
return null;
if (field.contains(".")) {
return super.getFieldValue(field);
}
Method getter = prop.getGetter();
if (null != getter) {
return invokeMethod(getter, entity);
}
Field fld = prop.getField();
if (null != fld) {
return getField(fld, entity);
}
return null;
return accessor.getProperty(entity.getPersistentProperty(field));
}
/*
* (non-Javadoc)
* @see org.springframework.validation.AbstractBindingResult#getTarget()
*/
@Override
public Object getTarget() {
return accessor.getBean();
}
}

View File

@@ -23,12 +23,14 @@ import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.RepositoryConstraintViolationException;
import org.springframework.data.rest.core.ValidationErrors;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.DirectFieldBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
@@ -167,8 +169,10 @@ public class ValidatingRepositoryEventListener extends AbstractRepositoryEventLi
Class<?> domainType = entity.getClass();
PersistentEntities persistentEntities = persistentEntitiesFactory.getObject();
Errors errors = new ValidationErrors(domainType.getSimpleName(), entity,
persistentEntities.getPersistentEntity(domainType));
PersistentEntity<?, ?> persistentEntity = persistentEntities.getPersistentEntity(domainType);
Errors errors = persistentEntity == null ? new DirectFieldBindingResult(entity, domainType.getSimpleName())
: new ValidationErrors(entity, persistentEntity);
for (Validator v : getValidatorsForEvent(event)) {

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.core;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
/**
* Unit tests for {@link ValidationErrors}.
*
* @author Oliver Gierke
*/
public class ValidationErrorsUnitTests {
KeyValueMappingContext context = new KeyValueMappingContext();
/**
* @see DATAREST-798
*/
@Test
public void exposesNestedViolationsCorrectly() {
ValidationErrors errors = new ValidationErrors(new Foo(), context.getPersistentEntity(Foo.class));
errors.pushNestedPath("bars[0]");
errors.rejectValue("field", "asdf");
errors.popNestedPath();
assertThat(errors.getFieldError().getField(), is("bars[0].field"));
}
static class Foo {
List<Bar> bars = new ArrayList<Bar>();
}
static class Bar {
String field;
}
}

View File

@@ -23,6 +23,7 @@ import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.RepositoryConstraintViolationException;
import org.springframework.data.rest.core.RepositoryTestsConfig;
@@ -59,10 +60,13 @@ public class ValidatorIntegrationTests {
}
@Autowired ConfigurableApplicationContext context;
@Autowired KeyValueMappingContext mappingContext;
@Test(expected = RepositoryConstraintViolationException.class)
public void shouldValidateLastName() throws Exception {
mappingContext.getPersistentEntity(Person.class);
// Empty name should be rejected by PersonNameValidator
context.publishEvent(new BeforeSaveEvent(new Person("Dave", "")));
}

View File

@@ -39,8 +39,8 @@ public class RepositoryConstraintViolationExceptionMessage {
String message = accessor.getMessage(fieldError);
this.errors.add(new ValidationError(fieldError.getObjectName(), message, String.format("%s",
fieldError.getRejectedValue()), fieldError.getField()));
this.errors.add(new ValidationError(fieldError.getObjectName(), message,
String.format("%s", fieldError.getRejectedValue()), fieldError.getField()));
}
}