How to manually trigger spring validation? - java

The annotated spring validation on fields of a POJO works when it is created from json request body. However, when I create the same object manually (using setters) and want to trigger validation, I'm not sure how to do that.
Here is the Registration class, which has Builder inner class that can build the object. In the build method I would like to trigger spring validation. Please scroll to the bottom and check Builder.build() and Builder.valiate() methods to see current implementation. I'm using javax.validation.Validator to trigger validation, but I prefer to leverage spring validation if possible.
package com.projcore.dao;
import com.projcore.util.ToString;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.hibernate.validator.constraints.NotEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.constraints.Size;
import java.util.List;
import java.util.Set;
/**
* The data transfer object that contains the information of a Registration
* and validation rules for attributes.
*/
#JsonIgnoreProperties(ignoreUnknown = true)
public final class Registration {
private static final Logger LOGGER = LoggerFactory.getLogger(Registration.class);
private String id;
#NotEmpty
#Size(max = 255)
private String messageId;
#NotEmpty
#Size(max = 255)
private String version;
#Size(max = 255)
private String system;
public Registration() {
}
private Registration(Builder builder) {
this.id = builder.id;
this.messageId = builder.messageId;
this.version = builder.version;
this.system = builder.system;
}
public static Builder getBuilder() {
return new Builder();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getMessageId() {
return messageId;
}
public void setMessageId(String messageId) {
this.messageId = messageId;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getSystem() {
return system;
}
public void setSystem(String system) {
this.system = system;
}
#Override
public String toString() {
return ToString.create(this);
}
/**
* Builder pattern makes the object easier to construct in one line.
*/
public static class Builder {
private String id;
private String messageId;
private String version;
private String system;
private Builder() {}
public Builder id(String id) {
this.id = id;
return this;
}
public Builder messageId(String messageId) {
this.messageId = messageId;
return this;
}
public Builder version(String version) {
this.version = version;
return this;
}
public Builder system(String system) {
this.system = system;
return this;
}
public Registration build() {
Registration entry = new Registration(this);
// *** Would like to trigger spring validation here ***
Set violations = validate(entry);
if (violations.isEmpty())
return entry;
else
throw new RuntimeException(violations.toString());
}
private Set validate(Registration entry) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Registration>> constraintViolations = validator.validate(entry);
return constraintViolations;
}
}
}
Validation works fine here:
#RequestMapping(method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
Registration create(#RequestBody #Valid Registration registration)
Solution:
Removed Registraion.Builder.validate(). Updated Registraion.Builder.build() to:
public Registration build() {
Registration entry = new Registration(this);
return (Registration) ValidatorUtil.validate(entry);
}
ValidationUtil.java
package projcore.util;
import com.ericsson.admcore.error.InvalidDataException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;
public class ValidatorUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ValidatorUtil.class);
private static final Validator javaxValidator = Validation.buildDefaultValidatorFactory().getValidator();
private static final SpringValidatorAdapter validator = new SpringValidatorAdapter(javaxValidator);
public static Object validate(Object entry) {
Errors errors = new BeanPropertyBindingResult(entry, entry.getClass().getName());
validator.validate(entry, errors);
if (errors == null || errors.getAllErrors().isEmpty())
return entry;
else {
LOGGER.error(errors.toString());
throw new InvalidDataException(errors.getAllErrors().toString(), errors);
}
}
}
InvalidDataException.java
package projcore.error;
import org.springframework.validation.Errors;
/**
* This exception is thrown when the dao has invalid data.
*/
public class InvalidDataException extends RuntimeException {
private Errors errors;
public InvalidDataException(String msg, Errors errors) {
super(msg);
setErrors(errors);
}
public Errors getErrors() {
return errors;
}
public void setErrors(Errors errors) {
this.errors = errors;
}
}

Spring provides full support for the JSR-303 Bean Validation API. This includes convenient support for bootstrapping a JSR-303 implementation as a Spring bean. This allows a javax.validation.Validator to be injected wherever validation is needed in your application.
Use the LocalValidatorFactoryBean to configure a default JSR-303 Validator as a Spring bean:
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
The basic configuration above will trigger JSR-303 to initialize using its default bootstrap mechanism. A JSR-303 provider, such as Hibernate Validator, is expected to be present in the classpath and will be detected automatically.
5.7.2.1 Injecting a Validator
LocalValidatorFactoryBean implements both javax.validation.Validator and org.springframework.validation.Validator. You may inject a reference to one of these two interfaces into beans that need to invoke validation logic.
Inject a reference to javax.validation.Validator if you prefer to work with the JSR-303 API directly:
// JSR-303 Validator
import javax.validation.Validator;
#Service
public class MyService {
#Autowired
private Validator validator;
}
Inject a reference to org.springframework.validation.Validator if your bean requires the Spring Validation API:
// Spring Validator
import org.springframework.validation.Validator;
#Service
public class MyService {
#Autowired
private Validator validator;
}
Here is a well exaplained example
Using JSR 303 with "classic" Spring Validators (enter the SpringValidatorAdapter)
This link is very helpful. Wrapping javax.validation.Validator in
org.springframework.validation.beanvalidation.SpringValidatorAdapter
helped deal with errors consistently. Can you add this as an answer so
that I can accept it
and
Spring doc here

Related

Micronaut error on calling repositry Context does not contain key: io.micronaut.tx.STATUS

import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.r2dbc.annotation.R2dbcRepository;
import io.micronaut.data.repository.reactive.ReactiveStreamsCrudRepository;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.runtime.Micronaut;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
}
#Controller
#RequiredArgsConstructor
class CustomerController {
private final CustomerRepository customerRepository;
#Get
public Flux<Customer> getAll() {
return customerRepository.findAll();
}
#Get("/test")
public Mono<Customer> test() {
return Mono.from(customerRepository.findById(1L)).map(e -> {
System.out.println(e);
return e;
});
// System.out.println(customerRepository);
// return Mono.just("TEST");
}
#Post
public Mono<Customer> create(#Body Customer customer) {
Customer saveCustomer = new Customer(customer.getName(), customer.getSurname());
return Mono.from(customerRepository.save(saveCustomer));
}
}
//#R2dbcRepository(dialect = Dialect.SQL_SERVER)
interface CustomerRepository extends ReactiveStreamsCrudRepository<Customer, Long> {
// #NonNull
// #Override
// Mono<Customer> findById(#NonNull #NotNull Long id);
//
// #NonNull
// #Override
// Mono<Customer> save(#NonNull #NotNull Customer customer);
//
#NonNull
#Override
Flux<Customer> findAll();
}
#MappedEntity("customer")
class Customer {
#Id
#GeneratedValue
private Long id;
private final String name;
private final String surname;
public Customer(String name, String surname) {
this.name = name;
this.surname = surname;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public String getSurname() {
return surname;
}
}
**
Trying to call a repositry through endpoint but keep getting error
**
Context does not contain key: io.micronaut.tx.STATUS
at reactor.util.context.Context1.get(Context1.java:67)
at io.micronaut.data.r2dbc.operations.DefaultR2dbcRepositoryOperations.lambda$withTransaction$27(DefaultR2dbcRepositoryOperations.java:441)
at reactor.core.publisher.FluxDeferContextual.subscribe(FluxDeferContextual.java:49)
at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:62)
at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54)
at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:62)
I've faced the same issue.
Check this:
#Get
public Flowable<Customer> getAll() {
return Flowable.fromPublisher(customerRepository.findAll());
}
and where is Mono use Single.fromPublisher(yourMono)
I encountered the same issue with micronaut 3.
After some tests i found that, in my specific case, this issue was caused by using the following modules:
micronaut-security-jwt
micronaut-reactor
micronaut-jackson-xml
If you remove all of these dependencies, the issue disappear.
I think that this problem is caused by the above micronaut modules, so i opened an issue on the micronaut-r2dbc github page with the hope they solve this issue ASAP.
EDIT:
they fixed this bug releasing the new micronaut version 3.0.1!!!

Validation not performed in a Spring controller

I want to use annotations in classes. I use javax.validation.constrants.* for annotations.
public final class EmailCredential implements Serializable {
private static final long serialVersionUID = -1246534146345274432L;
#NotBlank(message = "Sender must not be empty.")
#Email
private final String sender;
#NotBlank(message = "Subject must not be empty.")
private final String subject;
/// getters setters
}
None of them are working as expected. Meaning that when a below API gets called, annotations should throw error if annotated field is invalid. It looks like there is no annotation to check fields. How properly can I use annotations in a normal class?
controller:
#PostMapping(value = "/email/credentials", consumes = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> emailCredentials(#RequestBody EmailCredential emailCredential) {
return emailService.setCredentials(emailCredential);
}
In your case the validation has to be specified to be triggered.
So add the #Valid annotation on the parameter(s) that you want to validate such as :
import javax.validation.Valid;
// ...
#PostMapping(value = "/email/credentials", consumes = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> emailCredentials(#RequestBody #Valid EmailCredential emailCredential) {
return emailService.setCredentials(emailCredential);
}
According to Spring Boot official documentation : Validating Form Input
You should indicate that your EmailCredential need to be validated using the annotation #Valid
Here's an example from documentation :
package hello;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
#Controller
public class WebController implements WebMvcConfigurer {
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/results").setViewName("results");
}
#GetMapping("/")
public String showForm(PersonForm personForm) {
return "form";
}
#PostMapping("/")
public String checkPersonInfo(#Valid PersonForm personForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "form";
}
return "redirect:/results";
}
}

Cassandra entities must have the #Table, #Persistent or #PrimaryKeyClass Annotation

I am working on spring boot application and trying to connect with Data Stax Cassandra. Below is the I have written.
package com.sampleProj.dto;
import java.io.Serializable;
import java.sql.Blob;
import java.sql.Timestamp;
import org.springframework.data.cassandra.mapping.Column;
import org.springframework.data.cassandra.mapping.PrimaryKey;
import org.springframework.data.cassandra.mapping.Table;
#Table
public class Inbound implements Serializable{
#PrimaryKey
private int transactionalId;
#Column
private Timestamp received;
#Column
private String source;
#Column
private String service;
#Column
private Blob message;
public Inbound(int transactionalId, Timestamp received, String source, String service, Blob message) {
super();
this.transactionalId = transactionalId;
this.received = received;
this.source = source;
this.service = service;
this.message = message;
}
public Inbound() {
// TODO Auto-generated constructor stub
}
public int getTransactionalId() {
return transactionalId;
}
public void setTransactionalId(int transactionalId) {
this.transactionalId = transactionalId;
}
public Timestamp getReceived() {
return received;
}
public void setReceived(Timestamp received) {
this.received = received;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getService() {
return service;
}
public void setService(String service) {
this.service = service;
}
public Blob getMessage() {
return message;
}
public void setMessage(Blob message) {
this.message = message;
}
}
DAO:
package com.sampleProj.dao;
import org.springframework.data.cassandra.repository.CassandraRepository;
import com.sampleProj.dto.Inbound;
public interface TripDAO extends CassandraRepository<Inbound>{
}
Configuration:
package com.sampleProj;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
#SpringBootApplication
public class SampleProConfiguration {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(SampleProConfiguration .class, args);
}
}
CassandraConfiguration:
package com.sampleProj;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.cassandra.config.CassandraClusterFactoryBean;
import org.springframework.data.cassandra.config.java.AbstractCassandraConfiguration;
import org.springframework.data.cassandra.convert.MappingCassandraConverter;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.CassandraTemplate;
import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext;
import org.springframework.data.cassandra.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories;
#Configuration
#EnableCassandraRepositories
public class CassandraConfiguration extends AbstractCassandraConfiguration{
#Bean
#Override
public CassandraClusterFactoryBean cluster() {
CassandraClusterFactoryBean cluster = new CassandraClusterFactoryBean();
cluster.setContactPoints("localhost");
cluster.setPort(9042);
return cluster;
}
#Override
protected String getKeyspaceName() {
return "mykeyspace";
}
#Bean
#Override
public CassandraMappingContext cassandraMapping() throws ClassNotFoundException {
return new BasicCassandraMappingContext();
}
}
Dependencies:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-cassandra</artifactId>
</dependency>
But I am getting the exception as Cassandra entities must have the #Table, #Persistent or #PrimaryKeyClass Annotation. Please help me in resolving the issue. Thanks in advance.
Not the case in this example, but for future readers.. I received the same error when annotating my pojo/table class with:
#javax.persistence.Table
rather than:
#org.springframework.data.cassandra.mapping.Table
Your Inbound class does not know which table to refer in cassandra. Provide table name after #Table annotation like:
#Table(value="table_name_here").

What is the best way to implement custom validation in spring application?

I'm (new in spring development) creating REST API for my application, CRUD operations are implemented successfully but now I want to implement server side validation. I've also read that there are several ways through which validation could be implemented.
Using given annotations -> #notempty, #email, etc...
Using custom validation -> extending validators
I want to implement both of them in my application, With reference to that,
is it good approach to follow?
OR
Is there any other ways through which validation can be implemented?
Controller
#RestController
public class EmployeeController {
#Autowired
DataServices dataServices;
#Autowired
EmployeeValidator employeeValidator;
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(employeeValidator);
}
#RequestMapping(value = "/employee/", method = RequestMethod.POST)
public ResponseEntity<Object> createUser(
#Valid #RequestBody Employee employee,
UriComponentsBuilder ucBuilder) throws Exception,
DataIntegrityViolationException {
if (dataServices.addEmployee(employee) == 0) {
Error error = new Error(1, "Data integrity violation",
"Email id is already exists.");
return new ResponseEntity<Object>(error, HttpStatus.CONFLICT);
}
HttpHeaders headers = new HttpHeaders();
headers.setLocation(ucBuilder.path("/employee/{id}")
.buildAndExpand(employee.getId()).toUri());
Status status = new Status(1, "Employee has been added successfully.");
return new ResponseEntity<Object>(status, headers, HttpStatus.CREATED);
}
}
Error Handler
#ControllerAdvice
public class RestErrorHandler {
private static final Logger logger = Logger
.getLogger(RestErrorHandler.class);
private MessageSource messageSource;
#Autowired
public RestErrorHandler(MessageSource messageSource) {
this.messageSource = messageSource;
}
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ValidationErrorDTO processValidationError(
MethodArgumentNotValidException ex) {
logger.debug("Handling form validation error");
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
private ValidationErrorDTO processFieldErrors(List<FieldError> fieldErrors) {
ValidationErrorDTO dto = new ValidationErrorDTO();
for (FieldError fieldError : fieldErrors) {
String localizedErrorMessage = resolveLocalizedErrorMessage(fieldError);
dto.addFieldError(fieldError.getField(), localizedErrorMessage,
fieldError.getDefaultMessage());
}
return dto;
}
private String resolveLocalizedErrorMessage(FieldError fieldError) {
Locale currentLocale = LocaleContextHolder.getLocale();
String localizedErrorMessage = messageSource.getMessage(fieldError,
currentLocale);
// If a message was not found, return the most accurate field error code
// instead.
// You can remove this check if you prefer to get the default error
// message.
if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) {
String[] fieldErrorCodes = fieldError.getCodes();
localizedErrorMessage = fieldErrorCodes[0];
}
return localizedErrorMessage;
}
}
Validator
#Component
public class EmployeeValidator implements Validator {
public boolean supports(Class clazz) {
return Employee.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", errors
.getFieldError().getCode(), "First name is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", errors
.getFieldError().getCode(),
"Last name is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", errors
.getFieldError().getCode(),
"Email is required.");
}
}
Model
#Entity
#Table(name = "employee")
#JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue
#Column(name = "id")
private long id;
// #NotEmpty(message = "Please enter first name")
#Column(name = "first_name")
private String firstName;
// #NotEmpty(message = "Please enter last name")
#Column(name = "last_name")
private String lastName;
// #NotEmpty(message = "Please enter email address")
#Email(message = "Please enter valid email address")
#Column(name = "email", unique = true)
private String email;
#NotEmpty(message = "Please enter mobile number")
#Size(min = 10, message = "Please enter valid mobile number")
#Column(name = "phone")
private String phone;
//Getter and Setter
}
In your aproach you are using Server side validations but only in the controller layer. Have you tryied to use Bussines layer validations, like Hibernate Validation API http://hibernate.org/validator/
I've used it in a recent project and form me it's a great way to keep data consistent. Some tweaks and utils were needed to make it work as we wanted but it was not too difficult. For example, this validations, by default, are only checked just after persisting a Object in database, but in our controller we needed to make this validations earlier, so you we had to implement a way to call validation mechanism that relies on hibernate validation mechanism. Or, as another example, we had to develop a similar system on a web service to return errors when incoming data was not valid.
One way to use validations when needed is to implement it on all your bussines objects. They can inherit for a class like this:
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.xml.bind.annotation.XmlTransient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.fasterxml.jackson.annotation.JsonIgnore;
public abstract class BusinessObject implements Serializable, IObjectWithReport, IBusinessObject {
private static Log log = LogFactory.getLog(BusinessObject.class.getName());
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
#JsonIgnore
private Set<ConstraintViolation<BusinessObject>> errors;
/* Validation methods */
public final boolean valid() {
preValidate();
errors = new HashSet<ConstraintViolation<BusinessObject>>();
errors = validator.validate(this);
postValidate();
return errors.isEmpty();
}
/**
* Method to be overwriten in subclases so any BO can make some arrangement before checking valid
*/
protected void preValidate() {
log.trace("Generic prevalidate of " + this.getClass().getName());
}
/**
* Method to be overwriten in subclases so any BO can make some arrangement once validation has been made
*/
protected void postValidate() {
log.trace("Generic postValidate of " + this.getClass().getName());
}
public Set<ConstraintViolation<BusinessObject>> getErrors() {
return errors;
}
public boolean hasErrors() {
return errors != null && !errors.isEmpty();
}
}
Note that i use standard javax.validation.Validation API (check references here JPA 2.0 : what is javax.validation.* package?). But the implementation i use is the one from Hibernate.
Pros:
Validations are placed in one single layer, not spread along various layers. So they are easier to maintain.
Better model consistency because of that data is always validated in the same way, independently of how it was generated (user input, web service, pulled from other systems, etc).
Cons:
You need to develop some utils so you can use Model Validations in other layers, but it's not very dificult.
May be overkill if you have a simple project, whithout complexities like many info sources (user input, webservices input, rest services, other database systemas, etc) or interactions.

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.

Categories

Resources