Managing enums with Spring Data REST - java

I've a project with Spring Boot 1.5.7, Spring Data REST, Hibernate, HATEOAS, Spring validation.
I'm really struggling trying to manage enums. I need two simple things:
get the list of items of a specific enum (HTTP REST)
Sent an enum as parameter into a HTTP REST calls
I'm using Spring Data REST, so a lot of the work should be made from it.
My enum is something basic like:
public enum TransitCertificateStatus {
PENDING, USED, CANCELED, ARCHIVED
}
this enum is used into a bean:
#Entity
#EntityListeners(TransitCertificateListener.class)
public class TransitCertificate extends AbstractEntity {
private static final long serialVersionUID = 5978999252424024545L;
#NotNull(message = "{NotNull.transitcertificate.status}")
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private TransitCertificateStatus status = TransitCertificateStatus.PENDING;
To exposes enum items to the client I created a custom controller:
#Api(tags = "TransitCertificate Entity")
#RepositoryRestController
#RequestMapping(path = "/api/v1/transitCertificates")
public class TransitCertificateController {
private Logger log = LogManager.getLogger();
#RequestMapping(method = RequestMethod.GET, path = "/states", produces = "application/json")
public #ResponseBody ResponseEntity<?> getTransitCertificateStates() {
Resources<TransitCertificateStatus> resources = new Resources<TransitCertificateStatus>(
Arrays.asList(TransitCertificateStatus.values()));
return ResponseEntity.ok(resources);
}
This works fine, and the http response is:
{
"_embedded": {
"transitCertificateStatuses": [
"In attesa",
"Utilizzato",
"Annullato",
"Archiviato"
]
}
}
Note the items are translated because I set these values into rest-messages.properties and Spring Data REST takes care of the conversion.
server.model.enums.TransitCertificateStatus.PENDING = In attesa
server.model.enums.TransitCertificateStatus.USED = Utilizzato
server.model.enums.TransitCertificateStatus.CANCELED = Annullato
server.model.enums.TransitCertificateStatus.ARCHIVED = Archiviato
So far, everything is as expected.
Now I need to get a filtered list of by bean TransitCertificate and I am exposing directly the repository as a REST resource:
#RepositoryRestResource
#Transactional
#PreAuthorize("isAuthenticated()")
public interface TransitCertificateRepository extends PagingAndSortingRepository<TransitCertificate, Long> {
#Transactional(readOnly = true)
#Query("SELECT t FROM TransitCertificate t WHERE (:code IS NULL OR code=:code) AND (:date IS NULL OR DATE(createdDate)=DATE(:date)) AND (:text IS NULL OR text LIKE CONCAT('%',:text,'%')) AND (:from IS NULL OR t.from LIKE CONCAT('%',:from,'%')) AND (:customerName IS NULL OR customerName LIKE CONCAT('%',:customerName,'%')) AND (:states IS NULL OR status IN (:states)) ")
public Page<TransitCertificate> findAllByParameters(#Param("code") #RequestParam(value = "code", required = false) String code,
#Param("date") #RequestParam(value = "date", required = false) Instant date,
#Param("text") #RequestParam(value = "text", required = false) String text,
#Param("from") #RequestParam(value = "from", required = false) String from,
#Param("customerName") #RequestParam(value = "customerName", required = false) String customerName,
#Param("states") #RequestParam(value = "states", required = false) List<TransitCertificateStatus> states, Pageable pageable);
#Transactional(readOnly = true)
#Query("SELECT t FROM TransitCertificate t WHERE (:#{#filter} IS NULL OR t.status=:#{#filter.status}) ")
public Page<TransitCertificate> findAllByParameters2(#RequestBody #RequestParam(value="filter") #Param("filter") TransitCertificateFilter filter,
Pageable pageable);
The client sends enum translated values as the server replied in the first instance (it doesn't know anything else about the enum).
First approach
In the first approach I used a List<TransitCertificateStatus> as parameter but unfortunately I have an exception:
Could not write JSON: (was java.lang.NullPointerException); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: org.springframework.hateoas.Resources["_embedded"])
So I tried a different approach.
Second approach
This time I created a bean that is the filter and contains all parameters that should be filtered in the query.
public class TransitCertificateFilter implements Serializable {
private TransitCertificateStatus status;
public TransitCertificateStatus getStatus() {
return status;
}
public void setStatus(TransitCertificateStatus status) {
this.status = status;
}
}
In the method findAllByParameters2 so I use SpEL to get the property status from the bean the client sent.
This is the request:
{
"status":"Annullato"
}
And this is the exception I've back:
{
"timestamp": "2017-10-12T15:46:20.292+0000",
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.expression.spel.SpelEvaluationException",
"message": "EL1007E: Property or field 'status' cannot be found on null",
"path": "/api/v1/transitCertificates/search/findAllByParameters2"
}
I searched for a while info about this but I didn't find anything useful. Is someone able to put me on the right path?

Related

Validating multipart/form-data in Spring REST api

I recently came up to an issue related to validation. Typically, I am building a REST api that allow users to create their account including avatars. All of the information should be submitted when user clicks to Register button. So, my server will then receive a request that includes some fields like name (string), birthday (datetime), ... and avatar (multipart file). So, the question is how to validate the received file is a truly image and has an allowed size and simultaneously validate that the others (email, password) are also valid.
For the case that all fields is text, we can easily validate them using the combination of annotations like this
Controller
#PostMapping(path = "")
public ResponseEntity<?> createNewAccount(#RequestBody #Valid RegisterRequest registerRequest) {
Long resourceId = service.createNewCoderAccount(registerRequest);
return ResponseEntity.created(location(resourceId)).build();
}
Request DTO
#ConfirmedPassword
public class RegisterRequest extends BaseRequest implements ShouldConfirmPassword {
#NotBlank(message = "Field 'email' is required but not be given")
#Email
#Unique(message = "Email has been already in use", service = UserValidatorService.class, column = "email")
private String email;
#NotBlank(message = "Field 'password' is required but not be given")
#Size(min = 6, message = "Password should contain at least 6 characters")
private String password;
#NotBlank(message = "Field 'confirmPassword' is required but not be given")
private String confirmPassword;
#NotBlank(message = "Field 'firstName' is required but not be given")
private String firstName;
#NotBlank(message = "Field 'lastName' is required but not be given")
private String lastName;
}
Or in case that the request containing only file(s), we can absolutely do like this
Controller
#PostMapping(path = "/{id}")
public ResponseEntity<?> editChallengeMetadata(
#ModelAttribute ChallengeMetadataRequest request,
BindingResult bindingResult,
#PathVariable("id") Long id,
#CurrentUser User user
) throws BindException {
challengeMetadataRequestValidator.validate(request, bindingResult);
if (bindingResult.hasErrors()) {
throw new BindException(bindingResult);
}
Long challengeId = service.updateChallengeMetadata(id, request, user);
return ResponseEntity.ok(RestResponse.build(challengeId, HttpStatus.OK));
}
Validator
public class ChallengeMetadataRequestValidator implements Validator {
#Override
public boolean supports(#NonNull Class<?> aClass) {
return ChallengeMetadataRequest.class.isAssignableFrom(aClass);
}
#Override
public void validate(#NonNull Object o, #NonNull Errors errors) {
ChallengeMetadataRequest request = (ChallengeMetadataRequest) o;
if (request.getBanner() != null && !request.getBanner().isEmpty()) {
if (!List.of("image/jpeg", "image/png").contains(request.getBanner().getContentType())) {
errors.rejectValue("banner", "challenge.mime-type.not-supported", new String[]{request.getBanner().getContentType()}, "Mime-type is not supported");
}
}
}
}
As you seen above, if I wrap all data (including avatar) in a DTO class, I definitely write its own validator. But what will happen if then I have to write manually hundreds validators like that.
So, do anyone have any idea about it, typically, make the multipart/form-data request becomes simalar with application/json request ?
Thanks and regards,

How to describe Schema of my object by Open Api 3.0 with #annotation?

Is it ok to use annotation for this? I don't understand how to use Yaml.
Example:
#Operation(summary = "Get page of top by rating articles by title")
#ApiResponses(value = {
#ApiResponse(responseCode = "200 successful operation",
content = #Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = #Schema(???)))
})
#GetMapping(value = {"/articles/topByRating"})
#JsonView({JsonViews.Short.class})
#ResponseStatus(HttpStatus.OK)
public ItemsDto<Article> getArticlesTopByRating(#RequestParam int numberPage, #RequestParam int sizePage) {
return articleCrud.getTopByRating(numberPage, sizePage);
}
ItemsDto:
public class ItemsDto<E> {
Long total = 0L;
List<E> items;
}
Article:
public class Article{
private Author author;
private String title;
private String body;
}
https://swagger.io/docs/specification/data-models/dictionaries/
Free-Form Objects
"If the dictionary values can be of any type (aka free-form object), use additionalProperties: true:"
1. type: object
2. additionalProperties: true
I will try to use 'ref = myfile.yaml' property into #Schema annotation
The object are converted automatically to Schema by springdoc-openapi.
Note that on your DTOs, you can add the #Schema annotation as well to customise the different fields.

How to validate Spring-Boot mapped Entitys

Iam trying to validate the following Scheme with the Spring Validator:
#RestController
#RequestMapping("/bankcode-service")
#Validated
public class BankcodeController {
#Autowired
Delegator delegator;
#Autowired
Delegator delegator;
#Autowired
BankcodeRepository bankcodeRepository;
#DeleteMapping(path = "/bankcode", consumes = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public DeferredResult<ResponseEntity<String>> lock(#Valid HttpEntity<BankcodeJSONEntity> httpEntity) {
DeferredResult<ResponseEntity<String>> response = new DeferredResult<>();
if (httpEntity.getBody() == null) {
response.setResult(new ResponseEntity<>("The request was empty!", HttpStatus.BAD_REQUEST));
return response;
}
response.setResult(delegator.delegateUseCase(new LockBankcodeProd(bankcodeRepository, httpEntity.getBody())));
return response;
}
The DTO used looks like that:
#Data
public class BankcodeJSONEntity {
#NotNull
#Size(min = 8, max = 8)
private String bankcode;
#NotNull
#Size(min = 11, max = 11)
private String bic;
#NotNull
private String ticket;
#Basic
#Temporal(TemporalType.DATE)
#NotNull
private Date date;
#NotNull
private String category;
#NotNull
private String name;
}
But no matter if I pass in:
{"bankcode":"00000000", "bic":"AAAAAAAAAAA", "ticket":"SPOC-000000", "date":"2020-01-17", "category":"Fusion", "name":"Fantasiebank"}
Or an invalid one:
{"bankcode":"21750000", "bic":"AAAAAAAA", "ticket":"SPOC-000000", "date":"2020-01-17", "category":"Fusion", "name":"Fantasiebank"}
There is no constraintvalidationexception thrown. In many Tutorials I've seen that the validation is mostly done with concrete Arguments instead of a DTO. Is the DTO Validation possible because I can only have 7 Constructor Arguments before SonarLint lowers my Code Quality.
What am I doing wrong here?
Please remove HttpEntity from parameter. Change your parameter as #Valid BankcodeJSONEntity entity.
Because HttpEntity represents HTTP request or response including headers and body, usually used with RestTemplate. And for controller, usually as response wrapper.
public DeferredResult<ResponseEntity<String>> lock(#Valid BankcodeJSONEntityentity) {

boolean is set to false if it's not present in #RequestBody

I've stumbled upon interesting case and I'm not sure how to resolve it. It's probably related to JSON Post request for boolean field sends false by default but advices from that article didn't help.
Let's say I have this class:
public class ReqBody {
#NotNull
#Pattern(regexp = "^[0-9]{10}$")
private String phone;
//other fields
#NotNull
#JsonProperty(value = "create_anonymous_account")
private Boolean createAnonymousAccount = null;
//constructors, getters and setters
public Boolean getCreateAnonymousAccount() {
return createAnonymousAccount;
}
public void setCreateAnonymousAccount(Boolean createAnonymousAccount) {
this.createAnonymousAccount = createAnonymousAccount;
}
}
I also have endpoint:
#PostMapping(value = "/test", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<MyOutput> test(
#ApiParam(value = "information", required = true) #RequestBody ReqBody input
) {
//do something
}
problem is when I send my request body as:
{
"phone": "0000000006",
"create_anonymous_account": null
}
or just like
{
"phone": "0000000006"
}
it sets createAnonymousAccount to false.
I have checked, and it correctly recognises "create_anonymous_account": true
Is there any way to "force" null value in boolean field?
I really need to know if it was sent or no, and not to have default value.
You can use Jackson annotation to ignore the null fields. If the Caller doesn't send createAnonymousAccount then it will be null.
#JsonInclude(JsonInclude.Include.NON_NULL)
public class ReqBody {
#NotNull
#Pattern(regexp = "^[0-9]{10}$")
private String phone;
//other fields
#JsonProperty(value = "create_anonymous_account")
private Boolean createAnonymousAccount ;
}

Spring Boot multiple files upload with multiple objects

I need help I last time create the controller with the single file upload with an object and it's work for me like
My POJO class
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({ "file", "data" })
public class FileWithObject<T> {
#JsonProperty("file")
private MultipartFile file;
#JsonRawValue
#JsonProperty("data")
private T data;
}
MY REST CONTOLLER
#RequestMapping(value="/filestore/{bucket-uuid}/appsport.com/singleFileUploadWithObject/{folder}",
method = RequestMethod.POST)
#ResponseBody
// api work
public String singleFileUploadWithObject(
#PathVariable(name="bucket-uuid", required = true) String bucketUUId,
#PathVariable(name="folder", required = false) String folder,
FileWithObject rawData) {
return pingResponse;
}
My Postman result
That's all work for me. How to send the list of the objects through the postman or is possible to handle that way request like below rest controller
#RequestMapping(value="/filestore/{bucket-uuid}/appsport.com/listOfObjectsWithSingleFile/{folder}", method = RequestMethod.POST)
#ResponseBody
public String listOfObjectsWithSingleFile(
#PathVariable(name="bucket-uuid", required = true) String bucketUUId,
#PathVariable(name="folder", required = false) String folder,
Set<FileWithObject> rawData) {
return pingResponse;
}
How to handle list of objects
[{
"file": fileobject,
"data": "zyz"
},{
"file": fileobject,
"data": "zyz"
}]
I'm trying to creating the api for this blow task
I have done this by use of Meta-Data concept there are few changes I did in controller and bean
Controller
#RequestMapping(value="/filestore/{bucket-uuid}/appsport.com/listOfObjectsWithSingleFile/{folder}",
method = RequestMethod.POST)
#ResponseBody
public String listOfObjectsWithSingleFile(
#PathVariable(name="bucket-uuid") String bucketUUId, #PathVariable(name="folder") String folder,
FileWithObject objects) { // change this Set<FileWithObject> rawData to FileWithObject objects
return pingResponse;
}
Bean
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({ "file", "files", "data" })
public class FileWithObject<T> {
#JsonProperty("file")
private MultipartFile file;
#JsonProperty("files")
private List<MultipartFile> files;
#JsonRawValue
#JsonProperty("data")
private T data;
// work like (meta-data)
List<FileWithObject> rawData;
// getter setter
}
Image's For Request
Note:- I'm Still looking for this issue handle this in a blow way
#RequestMapping(value="/filestore/{bucketuuid}/appsport.com/listOfObjectsWithSingleFile/{folder}", method = RequestMethod.POST)
#ResponseBody
public String listOfObjectsWithSingleFile(#PathVariable(name="bucket-uuid", required = true) String bucketUUId,
#PathVariable(name="folder", required = false) String folder,Set<FileWithObject> rawData) {
return pingResponse;
}
hope it helps some one

Categories

Resources