I'm using hibernate validations for validating incoming requests in spring #RestController.
Problem: I want to reuse the same DTO object in multiple endpoints. But validate some fields only by condition (eg only on specific endpoints).
#RestController
public class ProductsServlet {
#GetMapping("/avail/product")
public Object avail(#Valid ProductDTO product) {
//should validate id field only
}
#GetMapping("/sell/product")
public Object sell(#Valid(with = GroupFuture.class) ProductDTO product) {
//should validate id + from field
}
}
public class ProductDTO {
#NotNull
#NotBlank
private String id;
#Future(groups = GroupFuture.class)
private Date from;
}
Of course the #Valid(with = GroupFuture.class) statement is invalid. But it shows what I'm trying to achieve.
Is that possible?
Got it. Also having to use the Default.class group to validate any fields not having a group.
#GetMapping("/sell/product")
public Object sell(#Validated({Default.class, GroupFuture.class}) ProductDTO product) {
}
Related
what I am trying to do is,
If I take one pojo class like
#Entity
#Table(name = "property_table")
public class Property {
#Id
#Column(name = "property_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int propertyId;
#Column(name = "property_name")
private String propertyName;
#Column(name = "property_type")
private String propertyType;
}
In RestController I wrote Two Methods like
#GetMapping(value = "/getProperties", produces = { "application/json",
"application/xml" }, consumes = { "application/xml", "application/json" })
#ResponseBody
public List<Property> getProperties() {
//some code
}
#GetMapping(value = "/getPropertyById", produces = { "application/json",
"application/xml" }, consumes = { "application/xml", "application/json" })
#ResponseBody
public Property getPropertyById() {
//some code
}
So, hear what I am trying to do is
for first api method I want return json like some parameters from Property pojo class i.e., like
for getProperties api method
{
"property":[
{
"propertyId":001,
"propertyName":"PROPERTY 1"
},
{
"propertyId":002,
"propertyName":"PROPERTY 2"
}
],
In the Above json I want to return only two parameters i.e propertyId,propertyName and remaining parameter i.e propertyType I dont want to retun in json.
How to return like that?
and for the second api method I want to return all three parameters. i.e., like below
for getPropertyById api method
{
"propertyId":001,
"propertyName":"PROPERTY 1",
"propertyType:"PROPERTY_TYPE 1"
},
how to maintain different json response using same pojo class with different parameters for different api methods.
please help me to solve this isuue.
Thanks.
REST API under/over-fetching is a well-known problem. There's only two (classical ways) to handle that.
The first one is to build one model per each attribute visibility state. So, in your case, you'll need to create two different models (this kind of models are called DTO - Data Transfert Object). One model will have a propertyType attribute, the other will not. The model Property you've shared shows that you use the same class as entity and as transfert object. This solution will add some complexity to your app because you will have to implement some mappers to convert your entity to a corresponding DTO.
The second one is to accept that you send an attribute that will not be useful (be aware of the over-fetching). This solution is often the most adopted one. The cons of this solution is when you don't want to send something to your client (imagine a User model, you want to get the password from your client but you don't want to sent it back to it). Another obvious negative point is that the transactions will be larger but it is negligible in most cases
I would strongly advice you to keep your #Entity isolated in the 'db' layer. So that changes on the database side don't affect your API and vice versa. Also, you will have much better control over what data is exposed in your API. For your needs you can create 2 true DTOs, like PropertyDto and PropertyDetailsDto (or using private fields and getters/setters).
public class PropertyDto {
public String propertyId;
public String propertyName;
}
public class PropertyDetailsDto extends PropertyDto {
public String propertyType;
}
Map your #Entity to a specific dto corresponding to your needs.
EDIT
public List<PropertyDto> getProperties() {
return toPropertyDtos(repository.findAll());
}
public PropertyDetailsDto getPropertyById(Long id) {
return toPropertyDetailsDto(repository.findBy(id));
}
in some Mapper.java
...
public static List<PropertyDto> toPropertyDtos(List<Property> properties) {
return properties.stream()
.map(Mapper::toPropertyDto)
.collect(toList());
}
private static PropertyDto toPropertyDto(Property property) {
PropertyDto dto = new PropertyDto();
dto.propertyId = property.propertyId;
dto.propertyName = property.propertyName;
return dto;
}
// same stuff for `toPropertyDetailsDto`, you could extract common mapping parts in a separate private method inside `Mapper`
...
Given a RESTful web service developed using the Spring Boot framework, I wanted a way to suppress the birthDate of all Users in the response. This is what I implemented after looking around for a solution :
#RestController
public class UserResource {
#Autowired
private UserDAOservice userDAOService;
#GetMapping("/users")
public MappingJacksonValue users() {
List<User> users = userDAOService.findAll();
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
.filterOutAllExcept("id", "name");
FilterProvider filters = new SimpleFilterProvider().addFilter(
"UserBirthDateFilter", filter);
MappingJacksonValue mapping = new MappingJacksonValue(users);
mapping.setFilters(filters);
return mapping;
}
}
However, when I hit the rest end point in the browser, I can still see the birth date of the user in the response :
{
"id": 1,
"name": "Adam",
"birthDate": "1980-03-31T16:56:28.926+0000"
}
Question 1 : What API can I use to achieve my objective?
Next, assuming that I want to adhere to HATEOAS in combination with filtering, how can I go about doing this. I am unable to figure out the APIs that can be used for using these two features together :
#GetMapping("/users/{id}")
public EntityModel<User> users(#PathVariable Integer id) {
User user = userDAOService.findById(id);
if (user == null) {
throw new ResourceNotFoundException("id-" + id);
}
EntityModel<User> model = new EntityModel<>(user);
WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).users());
model.add(linkTo.withRel("all-users"));
//how do I combine EntityModel with filtering?
return model;
}
Question 2 : How do I combine EntityModel with MappingJacksonValue?
Note : I am aware of #JsonIgnore annotation but that would apply the filter for all end points that use the domain; however, I want to restrict the filtering only to the two endpoints above.
Turns out for this to work, I have to add the #JsonFilter annotation above the DTO and provide the same name that was used while creating the SimpleFilterProvider.
#JsonFilter("UserBirthDateFilter")
public class User {
private Integer id;
#Size(min=2, message="user name must be atleast 2 characters")
#ApiModelProperty(notes="user name must be atleast 2 characters")
private String name;
#Past
#ApiModelProperty(notes="birth date cannot be in the past")
private Date birthDate;
//other methods
}
There is an easier way to do this, on your transfer object (the class you are sending back to the client), you can simply use the #JsonIgnore annotation to make sure the field is not serialized, and therefore sent to the client. So simply add #JsonIgnore inside your User class for your birthDay field.
You can also read more here about this approach:
https://www.baeldung.com/jackson-ignore-properties-on-serialization
If you need to return a different object for different endpoints (User without birthDay in your case, only for specific) you should create separate transfer objects and use those for their respective endpoints. You can pass your original entity (User) in the constructor to those classes and copy over all fields needed.
You can use Jackson's #JsonView feature. With this, you can tell a certain request mapping to produce serialized JSON with chosen set of properties.
public class View {
interface UserDetails {}
}
public class User {
#JsonView(View.UserDetails.class)
private Long id;
#JsonView(View.UserDetails.class)
private String name;
private String birthdate;
}
Controller be like
#JsonView(View.UserDetails.class)
#GetMapping("/users")
public MappingJacksonValue users() {
....
}
For question 2, I had the exact same question as you did, and here's what I did. It seems to be working:
#GetMapping(path = "/users/{id}")
public MappingJacksonValue retrieveUser(#PathVariable int id){
User user = service.findOne(id);
if(user==null){
throw new UserNotFoundException("id-"+id);
}
//"all-users", SERVER_PATH + "/users"
EntityModel<User> resource = EntityModel.of(user);
WebMvcLinkBuilder linkTo =
linkTo(methodOn(this.getClass()).retrieveAllUsers());
resource.add(linkTo.withRel("all-users"));
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("id");
FilterProvider filters = new SimpleFilterProvider().addFilter("UserFilter",filter);
MappingJacksonValue mapping = new MappingJacksonValue(resource);
mapping.setFilters(filters);
return mapping;
}
Response for HTTP GET localhost:8080/users/1
{
"id": 1,
"links": [
{
"rel": "all-users",
"href": "http://localhost:8080/users"
}
]}
I have created Post, Put and Delete Request in my controller in spring boot.
I have added validations in my model and also added #Valid parameter in method on controller.
I want to what else I am supposed to add for validation for Post, Put and Delete operation?
public class Employee {
#NotNull(message = "Employee Id can not be null")
private Integer id;
#Min(value = 2000, message = "Salary can not be less than 2000")
#Max(value = 50000, message = "Salary can not be greater than 50000")
private Integer salary;
#NotNull(message = "designation can not be null")
private String designation;
}
My Post Method is :
#PostMapping("/employees")
public ResponseEntity<Void> addEmployee(#Valid #RequestBody Employee newEmployee) {
Employee emp= service.addEmployee(newEmployee);
if (emp== null) {
return ResponseEntity.noContent().build();
}
return new ResponseEntity<Void>(HttpStatus.CREATED);
}
My Put Method is :
#PutMapping("/employees/{id}")
public ResponseEntity<Vehicle> updateEmployee(#Valid #RequestBody Employee updateEmployee) {
Employee emp= service.EmployeeById(updateEmployee.getId());
if (null == emp) {
return new ResponseEntity<Employee>(HttpStatus.NOT_FOUND);
}
emp.setSalary(updateEmployee.getSalary());
emp.setDesignation(updateEmployee.getDesignation());
service.updateEmployee(emp);
return new ResponseEntity<Employee>(emp, HttpStatus.OK);
}
Delete Method
#DeleteMapping("/employees/{id}")
public ResponseEntity<Employee> deleteEmployee(#Valid #PathVariable int id) {
Employee emp = service.getEmployeeById(id);
if (null == employee) {
return new ResponseEntity<Employee>(HttpStatus.FOUND);
}
service.deleteEmployee(id);
return new ResponseEntity<Employee>(HttpStatus.NO_CONTENT);
}
What is your specific problem?
Please refer to the following source for further reading.
Validation in Spring Boot
To your question about PUT - update does not work properly ?
Although, code looks ok. But if you are using JPA, then please remember JPA has delay data writing to database mechanism meaning it does not write data to database right away. And if you want JPA to write/save your data right away then you will have to call respository.saveAndFlush() - to force the JPA to write all data in session.
So, instead of calling the repository.saveAndFlush() every time you save data, you can simply return the same request object in this case "updateEmployee" instead of "emp" object for updating record e.g. :
return new ResponseEntity(updateEmployee, HttpStatus.OK);
POST : You should not use "#NotNull(message = "Employee Id can not be null")" on private Integer id since you are using same object for both POST and PUT method because #Valid will validate all fields in class.
As we all know, there is a big problem with a partial update of the entity. Since the automatic conversion from json strings to the entity, all fields that have not been transferred will be marked null. And as a result, the fields that we did not want to reset will be reset.
I will show the classical scheme:
#RestController
#RequestMapping(EmployeeController.PATH)
public class EmployeeController {
public final static String PATH = "/employees";
#Autowired
private Service service;
#PatchMapping("/{id}")
public Employee update(#RequestBody Employee employee, #PathVariable Long id) {
return service.update(id, employee);
}
}
#Service
public class Service {
#Autowired
private EmployeeRepository repository;
#Override
public Employee update(Long id, Employee entity) {
Optional<T> optionalEntityFromDB = repository.findById(id);
return optionalEntityFromDB
.map(e -> saveAndReturnSavedEntity(entity, e))
.orElseThrow(RuntimeException::new);
}
private T saveAndReturnSavedEntity(Employee entity, Employee entityFromDB) {
entity.setId(entityFromDB.getId());
return repository.save(entity);
}
}
#Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
and as I have already said that in the current implementation we will not be able to perform a partial update in any way. That is, it is impossible to send an update of only one field in a json line; all fields will be updated, and in null (excepted passed).
The solution to this problem is that you need to perform the conversion from string json to the entity in manual. That is, do not use all the magic from Spring Boot (which is very sad).
I will also give an example of how this can be implemented using merge at the json level:
#RestController
#RequestMapping(EmployeeRawJsonController.PATH)
public class EmployeeRawJsonController {
public final static String PATH = "/raw-json-employees";
#Autowired
private EmployeeRawJsonService service;
#PatchMapping("/{id}")
public Employee update(#RequestBody String json, #PathVariable Long id) {
return service.update(id, json);
}
}
#Service
public class EmployeeRawJsonService {
#Autowired
private EmployeeRepository employeeRepository;
public Employee update(Long id, String json) {
Optional<Employee> optionalEmployee = employeeRepository.findById(id);
return optionalEmployee
.map(e -> getUpdatedFromJson(e, json))
.orElseThrow(RuntimeException::new);
}
private Employee getUpdatedFromJson(Employee employee, String json) {
Long id = employee.getId();
updateFromJson(employee, json);
employee.setId(id);
return employeeRepository.save(employee);
}
private void updateFromJson(Employee employee, String json) {
try {
new ObjectMapper().readerForUpdating(employee).readValue(json);
} catch (IOException e) {
throw new RuntimeException("Cannot update from json", e);
}
}
}
#Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
With this solution, we eliminate the problem associated with the partial update.
But here another problem arises, that we are losing the automatic addition of validation of beans.
That is, in the first case, validation is enough to add one annotation #Valid:
#PatchMapping("/{id}")
public Employee update(#RequestBody #Valid Employee employee, #PathVariable Long id) {
return service.update(id, employee);
}
But we can't do the same when we perform manual deserialization.
My question is, is there any way to enable automatic validation for the second case?
Or maybe there are other solutions that allow you to use Spring Boot magic for Bean Validation.
What you need is not the normal validation , which can achieved through manual validator call.Let’s now go the manual route and set things up programmatically:
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<User>> violations = validator.validate(object);
for (ConstraintViolation<User> violation : violations) {
log.error(violation.getMessage());
}
To validate a bean, we must first have a Validator object, which is constructed using a ValidatorFactory.
Normal validations on Spring Controllers specified with #Valid annotations are triggered automatically during the DataBinding phase when a request is served.All validators registered with the DataBinder will be executed at that stage. We can't do that for your case, so you can manually trigger the validation like above.
I have created an index (house) with a type "apartments" that contains 20 documents. I uploaded the Json as a binary file into elasticsearch using postman. I have a Spring Boot project that has the following classes:
EsConfig.java - I have configured the clustername which is the default name in the application.properties file.
#Configuration
#EnableElasticsearchRepositories(basePackages = "com.search.repository")
public class EsConfig {
#Value("${elasticsearch.clustername}")
private String EsClusterName;
#Bean
public Client esClient() throws UnknownHostException {
Settings esSettings = Settings.builder()
.put("cluster.name", EsClusterName)
.put("client.transport.sniff", true)
.put("client.transport.ignore_cluster_name", false)
.build();
TransportClient client = new PreBuiltTransportClient(esSettings)
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost"), 9300));
return client;
}
#Bean
public ElasticsearchOperations elasticsearchTemplate() throws Exception{
return new ElasticsearchTemplate(esClient());
}
}
Apartments.java - This is my data model. The documents have the below fields in elasticsearch.
#Document(indexName = "house", type = "apartments")
#JsonIgnoreProperties(ignoreUnknown=true)
public class Apartments {
#Id
private String id;
#JsonProperty("Apartment_Name")
private String apartmentName;
#JsonProperty("Apartment_ID")
private String apartmentId;
#JsonProperty("Area_Name")
private String areaName;
//constructors along with getters and setters
}
ApartmentSearchRepository.java - This is an interface that extends the ElasticsearchRepository interface to perform crud operations.
public interface ApartmentSearchRepository extends ElasticsearchRepository<Apartments, String> {
List<Apartments> findByApartmentName(String apartmentName);
}
EsApartmentService.java -
#Service
public class EsApartmentService {
#Autowired
ApartmentSearchRepository apartmentSearchRepository;
public List<Apartments> getApartmentByName(String apartmentName) {
return apartmentSearchRepository.findByApartmentName(apartmentName);
}
}
ApartmentController.java - I have created an endpoint that should give back those 20 documents from elasticsearch. (Also, Apartment is a POJO in my project and Apartments is the data model.)
#Autowired
EsApartmentService esApartmentService;
#GetMapping(path = "/search",produces = "application/json")
public Set<Apartment> searchApartmentByName(
#RequestParam(value = "apartmentName", defaultValue = "") String apartmentName) throws IOException {
List<Apartment> apartments= new ArrayList<>();
esApartmentService.getApartmentByName(apartmentName).forEach(apartment-> {
apartments.add(new Apartment(apartment.getApartmentName(), apartment.getApartmentId(), apartment.getAreaName()));
});
return apartments.stream()
.collect(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Apartment::getApartmentId))));
}
This code gives back a status of 200 but with an empty response. I tried debugging but it seems that it is unable to read those documents from elasticsearch. I went through a couple of solutions but most of them have set the document data from within the code itself.
I am unable to retrieve those documents by hitting the endpoint I specified in the controller. Can someone let me know what I could be missing out on? Thanks! :)
Edit: The screenshot below shows the query and response in Postman.
As far I know, you are able to use #JsonProperty in order to map the POJO to the query response but you're loosing the ability to use the dynamic finder methods (findBy*) of spring data. The dynamic finders generation of spring data relies on reflection and there is where the field names in your POJO become important.
Would you mind to change the field names of you POJO or in your documents to verify this? Or just define a custom query? There is also a powerfull java api where you can define more complex queries: https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.misc.filter
As mentioned above by #ibexit, I removed #JsonProperty and used the native search query builder in my service. Also, it was not taking Apartment_Name and worked when I gave apartment_Name. (seems like Elasticsearch has case issues so I gave it in Camel Case.)
My changes:
Apartments.java - Removed #JsonProperty
#Document(indexName = "house", type = "apartments")
//#JsonIgnoreProperties(ignoreUnknown=true)
public class Apartments {
#Id
private String id;
//#JsonProperty("apartment_ID")
private String apartment_ID;
//#JsonProperty("Area_Name")
private String area_Name;
//#JsonProperty("Apartment_Name")
private String apartment_Name;
}
EsApartmentService.java -
#Service
public class EsApartmentService {
#Autowired
private ElasticsearchTemplate elasticsearchTemplate;
public List<Apartments> getApartmentByName(String apartmentName) {
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(org.elasticsearch.index.query.QueryBuilders
.matchQuery("apartment_Name", apartmentName)).build();
Page<Apartments> sampleEntities =
elasticsearchTemplate.queryForPage(searchQuery,Apartments.class);
return sampleEntities.getContent();
}
}
Removed ApartmentSearchRepository.java file.
These changes gave me the required response! :)