I'm using Spring Boot 1.5.4, Spring Data REST, HATEOAS. I'm exposing REST endpoints to be consumed from a Angular client.
I'm using spring.data.rest.enable-enum-translation=true to convert enums. It works fine both in GET and POST requests exposed from Spring Data REST from repositories.
I added a custom method in a repository:
#Transactional(readOnly = true)
#PreAuthorize("isAuthenticated()")
public interface TransitCertificateRepository extends PagingAndSortingRepository<TransitCertificate, Long> {
#Query("SELECT t FROM TransitCertificate t WHERE :states IS NULL OR status IN (:states) ")
public Page<TransitCertificate> findAllByParameters(
#Param("states") #RequestParam(value = "states", required = false) List<TransitCertificateStatus> states, Pageable pageable);
This is the enum:
public enum TransitCertificateStatus {
PENDING, USED, CANCELED, ARCHIVED
}
This is the relevant part of the model:
#Entity
#EntityListeners(TransitCertificateListener.class)
public class TransitCertificate extends AbstractEntity {
private static final long serialVersionUID = 5978999252424024545L;
#NotNull(message = "The status cannot be empty")
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private TransitCertificateStatus status = TransitCertificateStatus.PENDING;
In rest-messages.properties I've translation for the enum like:
server.model.enums.TransitCertificateStatus.PENDING = Pending
server.model.enums.TransitCertificateStatus.USED = Used
When the client try to call my method findAllByParameters and sends a array of String (translated how the server sent back), the conversion on the server fails.
I don't understand why the conversion works in save() method, for example, but not in my method.
Furthemore if the client sends me 2 states, Spring returns this error:
Parameter value element [USED] did not match expected type [server.model.enums.TransitCertificateStatus (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value element [USED] did not match expected type [server.model.enums.TransitCertificateStatus (n/a)]
So I guess I've two problems:
For some reason Spring is not able to convert a String[] to a List<TransitCertificateStatus> even if the value is exactly the one defined in the TransitCertificateStatus (PENDING, USED, CANCELED, ARCHIVED)
Spring is not able to convert the String the client send, to the right enum when the value is one of that defined in rest-messages.properties (Pending, Used, etc).
Is there a way to solve the problem is a elegant way using internal facilities of Spring Data REST (I point out enum transation works in save() method) without reinventing the wheel?
I ended up to solve the problem in this way:
I created a custom #RepositoryRestController
I created my method
I get the enum from the client like a String and then I convert it. In this way the client can send also the translated string for the enum
This a piece of code:
#PostMapping(path = "/licensePlates/searches")
public ResponseEntity<?> search(#RequestBody(required = true) List<Filter> filters, Pageable pageable, Locale locale,
PersistentEntityResourceAssembler resourceAssembler) {
EngineType engineType = enumTranslator.fromText(EngineType.class, filterMap.get("engineType"));
You have to inject enumTranslation in this way:
#Autowired
private EnumTranslator enumTranslator;
Not sure is the best way but that solved my problem with little code.
Related
I am currently studying Springboot so I am developing a personal project. In this Project a Have an entity named Campaign and I want to build a endpoint to list all campaigns in the database filtering, sorting and paginating
Some examples of requests:
# Pagination
GET domain/api/v1/campaigns (return the first page with all campaigns, default sorting and page size 20)
GET domain/api/v1/campaigns?page=2&size=50 (return the second page with all campaigns, default sorting and page size 50)
GET domain/api/v1/campaigns?page=2&size=100 (return the second page with all campaigns, default sorting and page size 50 which is the max)
# SORTING
GET domain/api/v1/campaigns?sort=name,asc (return the first page with all campaigns, sorting by name and page size 20)
GET domain/api/v1/campaigns?sort=name,asc&sort=startDate,dsc (return the first page with all campaigns, sorting by name and startDate and page size 20)
# Filtering
GET domain/api/v1/campaigns?status=open (return the first page with all campaigns with "open" status, default sorting and page size 20)
GET domain/api/v1/campaigns?status=open&status=new (return the first page with all campaigns with "open" and "new" status,default sorting and page size 20)
GET domain/api/v1/campaigns?status=open&startdate=2023-01-12 (return the first page with all campaigns with "open" status and startDate 2023-01-12, default sortin and page size 20)
# Combined
GET domain/api/v1/campaigns?status=open&status=new&page=2&size=10&sort=name,asc (returns page 2 with size 10 sorting by name the campaigns with statuses "open" and "new")
Which is the best way to implement this? A single Controller method that delegates to the correct service method based on the parameters? Collecting the parameters on a HashMap and create a generic Query for the database that deals with the parameters? Many different controllers that receive a diffent set of parameters?
Here is mt Campign Model and empty Repository, Service And Controller
#Data
#AllArgsConstructor
#NoArgsConstructor
#Entity
#Table(name = "CAMPAIGNS")
public class Campaign {
#Id
#GeneratedValue
private Long id;
private String name;
#Column(name = "start_date")
private LocalDateTime startDate;
#Column(name = "end_date")
private LocalDateTime endDate;
private String status;
}
public interface CampaignRepository extends JpaRepository<Campaign, Long> {
}
#Service
#AllArgsConstructor
public class CampaignService {
private final CampaignRepository campaignRepository;
#RestController
#AllArgsConstructor
#RequestMapping("/api/v1/campaigns")
public class CampaignController {
private final CampaignService campaignService;
}
It looks like you are trying to build a dynamic query. In this case, not only can you accomplish this with a single controller method, but you will also only need one service method with the use of Specifications. Additionally, you can retrieve certain pages and limit its size with the Page<T> interface and the PageRequest class
CampaignController.java:
// receive all possible arguments
public Page<Campaign> filterCampaigns(..., Pageable pageable) {
return campaignService.getCampaignsByCriteria(..., pageable.getPageNumber());
}
You can pass all possible URL parameters to the controller method. To execute specifications, CampaignRepository will need to extend the JpaSpecificationExecutor<T> interface. Paginating campaigns will require another interface PagingAndSortingRepository<T, ID>
CampaignRepository.java
public interface CampaignRepository extends
JpaRepository<Campaign, Long>, JpaSpecificationExecutor<Campaign>,
PagingAndSortingRepository<Campaign, Long> {
}
Following this, create a helper class that defines these specifications. Here's an example for finding by status, but you can have as many specifications as necessary
CampaignSpecification.java
public static Specification<Campaign> hasStatus(String campaignStatus) {
return startDate == null ? null : (root, query, criteriaBuilder) ->
criteriaBuilder.equal(root.get("status"), campaignStatus);
}
Returning the specification if a status value is provided or returning null otherwise makes it flexible because the status URL parameter is essentially optional this way. It's also worth noting that this is applicable for every parameter in the method. Now, you can execute this specification in the service.
CampaignService.java
public Page<Campaign> getCampaignsByCriteria(String status,
LocalDateTime startDate,
int pageNumber) {
PageRequest campaignPageRequest = PageRequest
.of(pageNumber, 2, Sort.by("startDate").ascending());
return campaignRepository.findAll(Specification.where(
CampaignSpecification.hasStatus(status).
and(CampaignSpecification.hasSomethingElse(...)))
, campaignPageRequest)
)
}
You can chain as many specifications as you need. The PageRequest class allows you to specify the current page, the maximum number of items for each page, as well as perform sorting
Further reading:
Spring Data Specification
Page interface
PageRequest class
For paging and sorting, the Spring Framework comes with good support:
public interface CampaignRepository extends PagingAndSortingRepository<Campaign, Long> {
}
PagingAndSortingRepository
This provides you the possibility to append a Pageable to the methods. E.g. if you want to page through results filtered by status:
public interface CampaignRepository extends PagingAndSortingRepository<Campaign, Long> {
List<Campaign> findAllByStatusIn(Collection<String> status, Pageable pageRequest);
}
Your Service can then easily prepare such a pageable, for example:
public List<Campaign> listByStatusSorted(Collection<String> status, int page, int pageSize, String sortProperty) {
PageRequest pageRequest = PageRequest.of(page, pageSize, Sort.by(sortProperty).ascending());
return campaignRepository.findAllByStatusIn(status, pageRequest);
}
PageRequest, Sort
Sorting ascending or descending can also be provided from the controller to the service of course. But in essence I would have one or two service methods. I think this is a matter of taste where you'd rather have another service method instead of adding ever more parameters.
I would try to mostly use one controller endpoint with default values. Something along the lines of:
public List<Campaign> listCampaigns(#RequestParam(value = "page", required = false, defaultValue = "0") int page, #RequestParam(value = "pageSize", required = false, defaultValue = "20") int pageSize, ...) {
...
}
Going by this, absolutely none of
Collecting the parameters on a HashMap and create a generic Query for the database that deals with the parameters
is needed. By using the naming conventions all is done for you. In fact, the repository I provided above should already work without any further code - just inject it in the service. Further reading on the power of Spring Data Repositories.
You could use the annotation #RequestParam in your controller. It would be something like:
#GetMapping("/api/v1/campaigns")
#ResponseBody
public String CampaignController(#RequestParam(name = "page", required = false, defaultValue = "0") Integer page, #RequestParam (name = "size", required = false, defaultValue = "50") Integer size) {
return "page: " + page;
}
TL;DR : Enum deserialization errors are not caught by org.springframework.validation.Errors in a Rest Controller
For reference: we didn't find a clean solution yet as we finally decided that no one should call us wit a bad enum
I have a rest controller that uses org.springframework.validation.Errors for parameter validations:
#RequestMapping(value = "/vol1/frodo")
public ResponseEntity<Object> simpleMethodUsingPost(
HttpServletRequest httpServletRequest,
#Valid #RequestBody MySimpleObject simpleObject,
Errors errors) {
/* If an error occured, I need to log the object */
if (errors.hasErrors()) {
List<FieldError> fields = errors.getFieldErrors();
doSomething(fields , simpleObject);
}
}
My class MySimpleObject looks like this:
public class MySimpleObject {
#Valid
#NotNull(message = "anObjectField is a mandatory field")
private EmbeddedObject anObjectField = null;
#Valid
#NotNull(message = "aStringField is a mandatory field")
private String aStringField = null;
#Valid
private MySimpleEnum aSimpleEnum = null;
}
And my enum class MySimpleEnum is basically a class with two values:
public enum MySimpleEnum{
ORC("ORC"),
URUK("URUK");
private String value;
MySimpleEnum(String value) {
this.value = value;
}
#Override
public String toString() {
return String.valueOf(value);
}
}
The validation of this object (and the injection of errors in the springframework Error object) works well when it's on a String or an Object, but it will fail validation of an enum (hence an object containing a valid-annoted enum will fail too).
It fails when trying to cast the JSON String to an enum when the value is not valid:
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error:
Cannot deserialize value of type 'lotr.middleearth.model.MySimpleEnum' from String "HOBBIT"
This deserialization error is caught if I use a ResponseEntityExceptionHandler and override handleHttpMessageNotReadable, but then I don't have access to the different other parameters and can't use them.
How can I configure either a Validator, enum or springframework Error so that this exception is caught and usable in my controller body?
I just came across the same problem but didn't like the idea of giving the user an unformatted "ugly" validation error message.
First, I made the enum property not nullable on the pojo.
#NotNull(message = "Type must be NEW_APPLICATION or RENEWAL")
private RegistrationSubmissionTypeEnum type;
Then I changed the setter to basically check the input (as a string) and see if it matches one of the enums. If not, I do nothing, the property stays null and it's reported back as one of the validation error messages (using the message text used on the #NotNull annotation).
public void setType(Object typeInput) {
for (RegistrationSubmissionTypeEnum typeEnum : RegistrationSubmissionTypeEnum.values()) {
if (typeEnum.getKey().equalsIgnoreCase(typeInput.toString())) {
this.type=RegistrationSubmissionTypeEnum.valueOf(typeInput.toString());
}
}
}
That's really the key. The normal behavior we all despise generates an ugly error message, but it also does it in a way such that this error message is displayed alone. Personally, I like to send back all errors en masse.
I'm not a fan of hardcoding the enum values on the #NotNull message, but in this particular case (small number of enum values), it's preferable to the default enum serialization error message, and the behavior of a one-off isolated error message.
I considered a custom validator, but that started to feel heavy. Maybe someone can improve on this.
The problem that is occurring is that in the enum MySimpleEnum there is no constant "HOBBIT" the possibilities are "ORC" and "URUK", in the validation question can be used simply as in the example:
#NotNull(message = "Custom message")
private MySimpleEnum aSimpleEnum
I ended up doing something like that to extract the problematic field in the request :
int start = ex.getMessage().indexOf("[\"");
int end = ex.getMessage().indexOf("\"]");
String fieldName = exception.getMessage().substring(start + 2, end)
The field happens to be at the end of the message between the brackets.
I'm not really proud of that one, it's messy, but it seems to be the only way with enums.
I guess it would be better to use strings and proper Spring validation instead, since it depends too much on the implementation and may break with future updates.
I am using springBoot 2 and I am trying to validate the objects in a List via:
#RequestMapping(value = "/bets",
produces = {"application/json"},
consumes = {"application/json"},
method = RequestMethod.POST
)
void postBets(#RequestBody List<#Valid Bet> bets);
and Bet class has #NotNull annotations on certain attributes.
import javax.validation.constraints.NotNull;
public class Bet extends BetMessage {
#NotNull
private String categoryName;
#NotNull
private String marketName = null;
#NotNull
private OffsetDateTime startTime = null;
#NotNull
private String betName = null;
I have also added the spring-boot-starter-validation artifact to my build file but still no validation is happening.
As a workaround I have implemented the popular answer in the question below (ValidList class) and validation is working as expected; however I think that I am missing something obvious and the solution is now part of the validation library.
Validation of a list of objects in Spring
You may want to write a wrapper which contains your list of Bet because then your wrapper will conform to JavaBean specs and the validations can be applied.
Below Answer might help in this case.
#Valid on list of beans in REST service
The following is just an exmaple for context not the actual implementation. Using Spring MVC, I have the following Model which has validation based on annotations.
#Entity
public class Customer {
#Id private int id;
#NotNull private String name;
}
And the following DTO used to map the data received in the request's body in the Controller's createNewCustomer function.
public class CustmerDTO{
private String name;
}
In my Controller I am using the modelMapper to convert the customerDTO to a new domain model Customer object. Based on the #NotNull annotation, if the name property of the recieved object (customerDTO) is empty the ConstraintViolationException is thrown.
public class CustomerController {
#Autowired private CustomerService customerService;
#Autowired private ModelMapper modelMapper;
#PostMapping(value = "/customer")
public Customer createNewCustomer (#RequestBody CustomerDTO customerDTO) {
try {
Customer newCustomer = modelMapper.map(customerDTO, Customer.class);
return customerService.saveCustomer(newCustomer);
}
catch (ConstraintViolationException e) {
throw new CustomerMissingInformation();
}
}
}
As you can see here I handle validation for the Customer in the Controller, which by definition is not a good habit, as Controllers in MVC are part of the presentation layer, and should have no clue about how to perform data validation, also I want to keep my controllers as light as possible.
Can I keep this design or is there any good approach to move my validation in the Service layer while keeping validation annotations and also allowing the Controller to be able to receive representation objects(DTOs) and convert them to domain models?
Looking more into this, I came to the following conclusion. Validation for persisting an object should happen as soon as possible and at property level, as you don't want your persistence layer or any mappers that you might be using deal with null when it is not expected. Everything that goes past property validation, shall be approached from a contextual validation perspective, and be passed to methods that hold logic for a given context.
Is this order valid to be filled, is this customer valid to check in to the hotel. So rather than have methods like isValid have methods like isValidForCheckIn.
I worked out a concept to conditionally validate using JSR 303 groups. "Conditionally" means that I have some fields which are only relevant if another field has a specific value.
Example: There is an option to select whether to register as a person or as a company. When selecting company, the user has to fill a field containing the name of the company.
Now I thought I use groups for that:
class RegisterForm
{
public interface BasicCheck {}
public interface UserCheck {}
public interface CompanyCheck {}
#NotNull(groups = BasicCheck.class)
private Boolean isCompany
#NotNull(groups = UserCheck.class)
private String firstName;
#NotNull(groups = UserCheck.class)
private String lastName;
#NotNull(groups = CompanyCheck.class)
private String companyName;
// getters / setters ...
}
In my controller, I validate step by step depending on the respective selection:
#Autowired
SmartValidator validator;
public void onRequest(#ModelAttribute("registerForm") RegisterForm registerForm, BindingResult result)
{
validator.validate(registerForm, result, RegisterForm.BasicCheck.class);
if (result.hasErrors()
return;
// basic check successful => we can process fields which are covered by this check
if (registerForm.getIsCompany())
{
validator.validate(registerForm, result, RegisterForm.CompanyCheck.class)
}
else
{
validator.validate(registerForm, result, RegisterForm.UserCheck.class);
}
if (!result.hasErrors())
{
// process registration
}
}
I only want to validate what must be validated. If the user selects "company" fills a field with invalid content and then switches back to "user", the invalid company related content must be ignored by the validator. A solution would be to clear those fields using Javascript, but I also want my forms to work with javascript disabled. This is why I totally like the approach shown above.
But Spring breaks this idea due to data binding. Before validation starts, Spring binds the data to registerForm. It adds error to result if, for instance, types are incompatible (expected int-value, but user filled the form with letters). This is a problem as these errors are shown in the JSP-view by <form:errors /> tags
Now I found a way to prevent Spring from adding those errors to the binding result by implementing a custom BindingErrorProcessor. If a field contains null I know that there was a validation error. In my concept null is not allowed - every field gets annotated with #NotNull plus the respective validation group.
As I am new to Spring and JSR-303 I wonder, whether I am totally on the wrong path. The fact that I have to implement a couple of things on my own makes me uncertain. Is this a clean solution? Is there a better solution for the same problem, as I think this is a common problem?
EDIT
Please see my answer here if you are interested in my solution in detail: https://stackoverflow.com/a/30500985/395879
You are correct that Spring MVC is a bit picky in this regard,and it is a common problem. But there are work-arounds:
Make all your backing fields strings, and do number/date etc conversions and null checks manually.
Use JavaScript to set fields to null when they become irrelevant.
Use JavaScript to validate fields when they are entered. This will fix almost all of your problems.
Good luck!
I know this question is old, but I came upon it looking for an answer for a different situation.
I think for your situation you could use inheritance for the forms and then use two controller methods:
The forms would look like this:
public class RegistrationForm
{
// Common fields go here.
}
public class UserRegistrationForm
extends RegistrationForm
{
#NotNull
private String firstName;
#NotNull
private String lastName;
// getters / setters ...
}
public class CompanyRegistrationForm
extends RegistrationForm
{
#NotNull
private String companyName;
// getters / setters ...
}
The controller methods would look like this:
#RequestMapping(method = RequestMethod.POST, params = "isCompany=false")
public void onRequest(
#ModelAttribute("registerForm") #Valid UserRegistrationForm form,
BindingResult result)
{
if (!result.hasErrors())
{
// process registration
}
}
#RequestMapping(method = RequestMethod.POST, params = "isCompany=true")
public void onRequest(
#ModelAttribute("registerForm") #Valid CompanyRegistrationForm form,
BindingResult result)
{
if (!result.hasErrors())
{
// process registration
}
}
Notice that the #RequestMapping annotations include a params attribute so the value of the isCompany parameter determines which method is called.
Also notice that the #Valid annotation is place on the form parameter.
Finally, no groups are needed in this case.