How to configure direct field access on #Valid in Spring? - java

How can I tell spring-web to validate my dto without having to use getter/setter?
#PostMapping(path = "/test")
public void test(#Valid #RequestBody WebDTO dto) {
}
public class WebDTO {
#Valid //triggers nested validation
private List<Person> persons;
//getter+setter for person
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public static class Person {
#NotBlank
public String name;
public int age;
}
}
Result:
"java.lang.IllegalStateException","message":"JSR-303 validated property
'persons[0].name' does not have a corresponding accessor for Spring data
binding - check your DataBinder's configuration (bean property versus direct field access)"}
Special requirement: I still want to add #AssertTrue on boolean getters to provide crossfield validation, like:
#AssertTrue
#XmlTransient
#JsonIgnore
public boolean isNameValid() {
//...
}

you have to configure Spring DataBinder to use direct field access.
#ControllerAdvice
public class ControllerAdviceConfiguration {
#InitBinder
private void initDirectFieldAccess(DataBinder dataBinder) {
dataBinder.initDirectFieldAccess();
}
}

Try something like this:
#Access(AccessType.FIELD)
public String name;

Related

Spring data rest validation for PUT and PATCH running after DB update

I have a SDR project and I am successfully validating the user entity for POST request but as soon as I update an existing entity using either PATCH or PUT the DB is updated BEFORE the validation is executed (the validator is being executed and error is returned but the DB is being updated anyway).
Do I need to setup a separate config for update ? Am I missing an extra step for that?
Entity
#Entity
#JsonIgnoreProperties(ignoreUnknown = true)
public class Member {
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "member_id_gen")
#SequenceGenerator(name = "member_id_gen", sequenceName = "member_id_seq")
#Id
#JsonIgnore
private long id;
#Version
private Integer version;
#NotNull
protected String firstName;
#NotNull
protected String lastName;
#Valid
protected String email;
}
Repository
#RepositoryRestResource(collectionResourceRel = "members", path = "member")
public interface MemberRepository extends PagingAndSortingRepository<Member, Long> {
public Member findByFirstName(String firstName);
public Member findByLastName(String lastName);
}
Validator
#Component
public class BeforeSaveMemberValidator implements Validator {
public BeforeSaveMemberValidator() {}
private String EMAIL_REGEX = "^[a-zA-Z0-9._%+-]+#[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$";
#Override
public boolean supports(Class<?> clazz) {
return Member.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Member member = (Member) target;
if(ObjectUtils.isEmpty(member.getFirstName())) {
errors.rejectValue("firstName", "member.firstName.empty");
}
if(ObjectUtils.isEmpty(member.getLastName())) {
errors.rejectValue("lastName", "member.lastName.empty");
}
if(!ObjectUtils.isEmpty(member.getDni()) && !member.getDni().matches("^[a-zA-Z0-9]*$")) {
errors.rejectValue("dni", "member.dni.invalid");
}
if(!ObjectUtils.isEmpty(member.getEmail()) && !member.getEmail().matches(EMAIL_REGEX)) {
errors.rejectValue("email", "member.email.notValid");
}
}
}
BeforeSave service
#Service
#RepositoryEventHandler(Member.class)
public class MemberService {
#HandleBeforeCreate
#HandleBeforeSave
#Transactional
public void beforeCreate(Member member) {
...
}
}
I think you should rename your validator, for example, to MemberValidator then assign it as described here:
#Override
protected void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener v) {
v.addValidator("beforeCreate", new MemberValidator());
v.addValidator("beforeSave", new MemberValidator());
}
But I suggest you to use Bean validation instead of your custom validators. To use it in SDR project you can inject LocalValidatorFactoryBean, then assign it for 'beforeCreate' and 'beforeSave' events in configureValidatingRepositoryEventListener:
#Configuration
#RequiredArgsConstructor // Lombok annotation
public class RepoRestConfig extends RepositoryRestConfigurerAdapter {
#NonNull private final LocalValidatorFactoryBean validatorFactoryBean;
#Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener v) {
v.addValidator("beforeCreate", validatorFactoryBean);
v.addValidator("beforeSave", validatorFactoryBean);
super.configureValidatingRepositoryEventListener(v);
}
}
In this case your SDR will automatically validate payloads of POST, PUT and PATCH requests for all exposed SDR repositories.
See my example for more details.

GET request with nested objects in #RestController?

Is it possible to create a GET webservice in spring and using nested properties in the query? Like search.limitResults in the following example:
localhost:8080/firstname=test&search.limitResults=10
You get the idea. Can this be achieved?
#RestController
public class MyServlet {
#RequestMapping(value = "/", method = RequestMethod.GET)
private String test(RestParams p) {
}
}
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
public class RestParams {
private String firstname;
private String lastname;
//is that possible to nest?
private Search search;
}
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
public class Search {
private int limitResults;
//some more
}
To answer my own question: it just works this way! Nested properties can be accessed using the dot accessor, like search.limitResults.

Specify field is transient for MongoDB but not for RestController

I'm using spring-boot to provide a REST interface persisted with MongoDB. I'm using the 'standard' dependencies to power it, including spring-boot-starter-data-mongodb and spring-boot-starter-web.
However, in some of my classes I have fields that I annotate #Transient so that MongoDB does not persist that information. However, this information I DO want sent out in my rest services. Unfortunately, both MongoDB and the rest controller seem to share that annotation. So when my front-end receives the JSON object, those fields are not instantiated (but still declared). Removing the annotation allows the fields to come through in the JSON object.
How I do configure what is transient for MongoDB and REST separately?
Here is my class
package com.clashalytics.domain.building;
import com.clashalytics.domain.building.constants.BuildingConstants;
import com.clashalytics.domain.building.constants.BuildingType;
import com.google.common.base.Objects;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import java.util.*;
public class Building {
#Id
private int id;
private BuildingType buildingType;
private int level;
private Location location;
// TODO http://stackoverflow.com/questions/30970717/specify-field-is-transient-for-mongodb-but-not-for-restcontroller
#Transient
private int hp;
#Transient
private BuildingDefense defenses;
private static Map<Building,Building> buildings = new HashMap<>();
public Building(){}
public Building(BuildingType buildingType, int level){
this.buildingType = buildingType;
this.level = level;
if(BuildingConstants.hpMap.containsKey(buildingType))
this.hp = BuildingConstants.hpMap.get(buildingType).get(level - 1);
this.defenses = BuildingDefense.get(buildingType, level);
}
public static Building get(BuildingType townHall, int level) {
Building newCandidate = new Building(townHall,level);
if (buildings.containsKey(newCandidate)){
return buildings.get(newCandidate);
}
buildings.put(newCandidate,newCandidate);
return newCandidate;
}
public int getId() {
return id;
}
public String getName(){
return buildingType.getName();
}
public BuildingType getBuildingType() {
return buildingType;
}
public int getHp() {
return hp;
}
public int getLevel() {
return level;
}
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
public BuildingDefense getDefenses() {
return defenses;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Building building = (Building) o;
return Objects.equal(id, building.id) &&
Objects.equal(hp, building.hp) &&
Objects.equal(level, building.level) &&
Objects.equal(buildingType, building.buildingType) &&
Objects.equal(defenses, building.defenses) &&
Objects.equal(location, building.location);
}
#Override
public int hashCode() {
return Objects.hashCode(id, buildingType, hp, level, defenses, location);
}
}
As is, hp and defenses show up as 0 and null respectively. If I remove the #Transient tag it comes through.
As long as you use org.springframework.data.annotation.Transient it should work as expected. Jackson knows nothing about spring-data and it ignores it's annotations.
Sample code, that works:
interface PersonRepository extends CrudRepository<Person, String> {}
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;
#Document
class Person {
#Id
private String id;
private String name;
#Transient
private Integer age;
// setters & getters & toString()
}
#RestController
#RequestMapping("/person")
class PersonController {
private static final Logger LOG = LoggerFactory.getLogger(PersonController.class);
private final PersonRepository personRepository;
#Autowired
PersonController(PersonRepository personRepository) {
this.personRepository = personRepository;
}
#RequestMapping(method = RequestMethod.POST)
public void post(#RequestBody Person person) {
// logging to show that json deserialization works
LOG.info("Saving person: {}", person);
personRepository.save(person);
}
#RequestMapping(method = RequestMethod.GET)
public Iterable<Person> list() {
Iterable<Person> list = personRepository.findAll();
// setting age to show that json serialization works
list.forEach(foobar -> foobar.setAge(18));
return list;
}
}
Executing POST http://localhost:8080/person:
{
"name":"John Doe",
"age": 40
}
Log output Saving person: Person{age=40, id='null', name='John Doe'}
Entry in person collection:
{ "_id" : ObjectId("55886dae5ca42c52f22a9af3"), "_class" : "demo.Person", "name" : "John Doe" } - age is not persisted
Executing GET http://localhost:8080/person:
Result: [{"id":"55886dae5ca42c52f22a9af3","name":"John Doe","age":18}]
I solved by using #JsonSerialize. Optionally you can also opt for #JsonDeserialize if you want this to be deserailized as well.
#Entity
public class Article {
#Column(name = "title")
private String title;
#Transient
#JsonSerialize
#JsonDeserialize
private Boolean testing;
}
// No annotations needed here
public Boolean getTesting() {
return testing;
}
public void setTesting(Boolean testing) {
this.testing = testing;
}
The problem for you seems to be that both mongo and jackson are behaving as expected. Mongo does not persist the data and jackson ignores the property since it is marked as transient. I managed to get this working by 'tricking' jackson to ignore the transient field and then annotating the getter method with #JsonProperty. Here is my sample bean.
#Entity
public class User {
#Id
private Integer id;
#Column
private String username;
#JsonIgnore
#Transient
private String password;
#JsonProperty("password")
public String getPassword() {
return // your logic here;
}
}
This is more of a work around than a proper solution so I am not sure if this will introduce any side effects for you.
carlos-bribiescas,
what version are you using for it. It could be version issue. Because this transient annotation is meant only for not persisting to the mongo db. Please try to change the version.Probably similar to Maciej one (1.2.4 release)
There was issue with json parsing for spring data project in one of the version.
http://www.widecodes.com/CyVjgkqPXX/fields-with-jsonproperty-are-ignored-on-deserialization-in-spring-boot-spring-data-rest.html
Since you are not exposing your MongoRepositories as restful endpoint with Spring Data REST it makes more sense to have your Resources/endpoint responses decoupled from your domain model, that way your domain model could evolve without impacting your rest clients/consumers. For the Resource you could consider leveraging what Spring HATEOAS has to offer.
I solved this question by implementing custom JacksonAnnotationIntrospector:
#Bean
#Primary
ObjectMapper objectMapper() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
AnnotationIntrospector annotationIntrospector = new JacksonAnnotationIntrospector() {
#Override
protected boolean _isIgnorable(Annotated a) {
boolean ignorable = super._isIgnorable(a);
if (ignorable) {
Transient aTransient = a.getAnnotation(Transient.class);
JsonIgnore jsonIgnore = a.getAnnotation(JsonIgnore.class);
return aTransient == null || jsonIgnore != null && jsonIgnore.value();
}
return false;
}
};
builder.annotationIntrospector(annotationIntrospector);
return builder.build();
}
This code makes invisible org.springframework.data.annotation.Transient annotation for Jackson but it works for mongodb.
You can use the annotation org.bson.codecs.pojo.annotations.BsonIgnore instead of #transient at the fields, that MongoDB shall not persist.
#BsonIgnore
private BuildingDefense defenses;
It also works on getters.

Spring class RequestBody and MongoRepository

I have class:
class TestClass {
#Id
private ObjectId id;
private ObjectId parentId;
private String name;
private String describe;
private String privateData;
public TestClass(ObjectId parentId, String name, String describe, String privateData) {
this.parrentId = parrentId;
this.name = name;
this.describe = describe;
this.privateDate = privateData;
}
// get/set methods...
}
Can I use this class in MongoRepository and #RequestBody? Is it safe? parrentId and privateData is private properties and RequestBody does not have to fill them.
mongorepository:
public interface TestClassRepository extends MongoRepository<TestClass, String> {
public TestClass findById(ObjectId id);
}
post method:
#RequestMapping(value="/testclass", method=RequestMethod.POST)
public void create(#RequestBody TestClass testClass) {
testClass.setParentId(...);
repo.insert(testClass);
}
For example:
{"name": "test", "describe": "test", "id": "54d5261a8314fe3c650d5b1d", "parentId": "54d5261a8314fe3c650d5b1d", "privateData": "WrongPrivateData"}
How can I do that it was impossible to set properties id, parentId, privateDate?
Or need I create new class for RequestBody? I don't want duplicate code.
It should be better and safe to use separate models for DAO and VO layers(view). If your models currently looks the same, it doesn't mean that they will stay the same in future. You can use the Dozer Mapping framework for mappings between your models. It's easy,fast and safe.
If you need to skip some field from mongotemplate mapping use #Transient annotation.
P.S. You don't need findById method, because mongotemplate already have find method which uses key as param. TestClass should have an empty constructor.

Hibernate Validator not invoked on Object Graph

I have a model object which has entities annotated with hibernate validation annotations. For example #NotBlank, #NotNull, #Length.
I have a form backing this model which decorates an instance of the model object. I have annotated this instance with an #NotNull, #Valid annotation. I am registering a validator for this backing form in the Controller Class and the form validator is being invoked when the #RequestMapping method argument is annotated with #Valid annotation.
Note the model is also annotated with the #Entity annotation, the model backing form is just
a thin wrapper around the model.
However the validations on the decorated object are not being checked? I know this because in the request mapping method definition I check the BindResult for errors and there are none.
My form fields are all empty, hence the validations on the fields on the decorated model annotated with #NotBlank should be checked. However that does not happen.
Can you help me fix this?
Edit:
Sample Code
#Entity
class MyModel {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(nullable=false)
private Long id;
#NotBlank
#Column(unique=true, length=30, nullable=false)
private String number;
#Column(length=30, nullable=false)
#Length(min=1, max=30)
private String firstName;
/* ... getters and setters ... */
}
public class MyModelBackingForm {
#NotNull
#Valid
private MyModel model;
/* ... delegate getters and setters for all fields in MyModel ... */
}
Edit:
Add Controller Code
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new MyBackingFormValidator());
}
Edit:
public class MyBackingFormValidator implements Validator {
public MyBackingFormValidator() {
super();
}
#Override
public boolean supports(Class<?> clazz) {
return Arrays.asList(MyBackingForm.class, MyModel.class).contains(clazz);
}
#Override
public void validate(Object obj, Errors errors) {
// custom validation code commented ... as I want to check if JSR 303 validations invoked
}
}
Here is a sample code the way I was using the hibernate validation. I was calling #valid on the fetched objects with the binding result. This way it knows the validations which are required to be done on the form.
Edited: How to invoke custom and standard validator?
Using #InitBinder we can pass the custom validator instance to which again we can pass the class parameter for the standard validator using binder.getValidator()
Courtesy: Rohit Banga
#Controller
#RequestMapping("/customer")
public class CustomerController {
//Edited:
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new CustomFormValidator(binder.getValidator()));
}
#RequestMapping(value = "/signup", method = RequestMethod.POST)
public String addCustomer(#Valid Customer customer, BindingResult result) {
if (result.hasErrors()) {
return "form";
} else {
return "success";
}
}
#RequestMapping(method = RequestMethod.GET)
public String customerForm(ModelMap model) {
model.addAttribute("customer", new Customer());
return "form";
}
}

Categories

Resources