Make Sort ignore field in model - java

I've encountered a problem:
I'm accepting the sortBy string in the controller and creating Sort object with Sort.by(sortBy).
The problem is that I can't seem to find a way to block fields from being sorted.
For example, I have:
#Column(nullable = false)
private String encryptedPassword;
I would like to block being able to sort by a password.
My controller method:
#GetMapping(produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_XML_VALUE }, consumes = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public PagedModel<EntityModel<UserDetailsResponse>> getUsers(#RequestParam(required = false, defaultValue = "0") Integer page,
#RequestParam Integer size,
#RequestParam(required = false, defaultValue = "createDate") String sortBy,
#RequestParam(required = false) String order) {
Sort.Direction sortDirection = Sort.Direction.fromOptionalString(order)
.orElse(Sort.Direction.DESC);
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(sortDirection, sortBy));
Page<UserDTO> users = userService.getUsers(pageRequest);
Page<UserDetailsResponse> userDetailsResponses = users.stream()
.map((userDTO) -> modelMapper.map(userDTO, UserDetailsResponse.class))
.map(this::addRelations)
.collect(Collectors.collectingAndThen(Collectors.toList(), PageImpl::new));
return pagedResourcesAssembler.toModel(userDetailsResponses);
}
Of course, I could create a blacklist with field names, but I'm looking for a non-hardcoding way.
Is there some annotation or other way I could use to achieve that?

Since there doesn't appear to be any annotation for that, I wrote my own.
If someone stumbles upon this problem here's the code:
Annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface SortBlacklisted {
}
SortBlackListUtil:
#Component
public class SortBlackListUtil {
/**
* Looks for fields annotated with #SortBlacklisted in the specified class
* and creates blacklist form their names.
*
* #param classToLookIn Class to search in for #SortBlacklisted annotation.
* #param <T> Class object type.
* #return List of blacklisted fields names for the specified class.
*/
public <T> ArrayList<String> getBlackListedFields(Class<T> classToLookIn) {
ArrayList<String> blackListedFieldsNames = new ArrayList<>();
for (Field field : classToLookIn.getDeclaredFields()) {
SortBlacklisted sortBlacklisted = field.getAnnotation(SortBlacklisted.class);
if (sortBlacklisted != null) blackListedFieldsNames.add(field.getName());
}
return blackListedFieldsNames;
}
}
Now simply place that an annotation on the desired field
#SortBlacklisted
private String encryptedPassword;
Use that method to get a list of blacklisted fields names:
sortBlackListUtil.getBlackListedFields(MyClass.class)

Related

Multiple Sort Optional query - Spring REST Controller configuration with Pagination

I want to build a spring controller that can handle multiple, optional sort queries. According to spring.io spec these queries should be formatted thus
&sort=name,desc&sort=code,desc
As discussed by EduardoFernandes
I know this can be done with one instance of sort with value to be sorted and the direction give separately as per Gregg but that doesn't match the Spring spec or handle multiple sort values.
I'm not sure how to turn multiple queries in the spring spec format into a Sort that I can pass to my PageRequest and then on to my repository. Also I would like the ability to make these optional and if possible, it would be great if I could use #Anotation based config if defaults are necessary to achieve this as per Rasheed Amir (#SortDefault)
Here is the basics of what I'm working with..
Domain
#Entity
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Subject {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String code;
...
Repository
public interface SubjectRepository extends JpaRepository<Subject, Long> {
}
Service
#Override
public Page<SubjectDTO> listSubjectsPageble(PageRequest pageableRequest) {
return subjectRepository.findAll(pageableRequest)
.map(subjectMapper::subjectToSubjectDTO);
}
Controller
#GetMapping
#ResponseStatus(HttpStatus.OK)
#PreAuthorize("hasRole('LECTURER')")
public Page<SubjectDTO> getSubjects(
#RequestParam("page") int page,
#RequestParam("size") int size,
#RequestParam("sort") String sort
) {
return subjectService.listSubjectsPageble(PageRequest.of(page, size, new Sort(sort)));
}
So here in the controller I don't know how to deal with\populate the Sort from the RequestParam at all, according to Ralph I should be able to use something like the below to get multiple values from one param, but I don't know how to then pass that to a Sort.
I know a Sort can take more than one parameter but only one sort direction. And then of coarse I would like to make them optional.
#RequestParam MultiValueMap<String, String> params
Please help, I'm still quite a noob :)
Thanks
EDIT
I solved some of my issues thanks to a post by Dullimeister But the approach feels a little messy and still doesn't handle multiple sort parameters. Does anyone know of better approach or is this the way to go?
#GetMapping
#ResponseStatus(HttpStatus.OK)
#PreAuthorize("hasRole('LECTURER')")
public Page<SubjectDTO> getSubjects(
#RequestParam(value = "page", defaultValue = "0", required = false) int page,
#RequestParam(value = "size", defaultValue = "10", required = false) int size,
#RequestParam(value = "sort", defaultValue = "name,ASC", required = false) String sortBy
) {
String[] sort = sortBy.split(",");
String evalSort = sort[0];
String sortDirection = sort[1];
Sort.Direction evalDirection = replaceOrderStringThroughDirection(sortDirection);
Sort sortOrderIgnoreCase = Sort.by(new Sort.Order(evalDirection,evalSort).ignoreCase());
return subjectService.listSubjectsPageble(PageRequest.of(page, size, sortOrderIgnoreCase));
}
private Sort.Direction replaceOrderStringThroughDirection(String sortDirection) {
if (sortDirection.equalsIgnoreCase("DESC")){
return Sort.Direction.DESC;
} else {
return Sort.Direction.ASC;
}
}
Final Solution
Thanks everyone, this is what I ended up with. Not sure if its the perfect way but it works :) I had to replace the comma with a semi-colon in the end as the FormattingConversionService was automatically parsing a single sort param to a string instead of an Sting[]
#GetMapping
#ResponseStatus(HttpStatus.OK)
#PreAuthorize("hasRole('LECTURER')")
public Page<SubjectDTO> getSubjects(
#RequestParam(value = "page", defaultValue = "0", required = false) int page,
#RequestParam(value = "size", defaultValue = "10", required = false) int size,
#RequestParam(value = "sort", defaultValue = "name;ASC", required = false) String[] sortBy
Sort allSorts = Sort.by(
Arrays.stream(sortBy)
.map(sort -> sort.split(";", 2))
.map(array ->
new Sort.Order(replaceOrderStringThroughDirection(array[1]),array[0]).ignoreCase()
).collect(Collectors.toList())
);
return subjectService.listSubjectsPageble(PageRequest.of(page, size, allSorts));
private Sort.Direction replaceOrderStringThroughDirection(String sortDirection) {
if (sortDirection.equalsIgnoreCase("DESC")){
return Sort.Direction.DESC;
} else {
return Sort.Direction.ASC;
}
Why don't you use Pageable in your controller ?
Pageable can handle many sort queries, each of them will be stored in orders list.
Moreover, any of pageable parameters aren't required. When you don't pass them in url, pageable will contains default values (page = 0, size = 20). You can change default values by using #PageableDefault annotation.
GET .../test?sort=name,desc&sort=code,desc

Is it possible to split request params in Spring controllers?

I have a request like:
example.com/search?sort=myfield1,-myfield2,myfield3
I would like to split those params to bind a List<String> sort in my controller or List<SortParam> where SortParam is the class with fields like: name (String) and ask (boolean).
So the final controller would look like this:
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<MyResponse> search(#RequestParam List<String> sort) {
//...
}
or
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<MyResponse> search(#RequestParam List<SortParam> sort) {
//...
}
Is there a way to make it?
UPDATE:
The standard way of passing parameters does not satisfy my requirements. I.e. I cannot use sort=myfield1&sort=-myfield2&sort=myfield3. I have to use comma separated names.
Also, I do understand that I can accept #RequestParam String sort in my controller and then split the string inside the controller like sort.split(",") but it also doesn't solve the above problem.
Its just a simple Type Covertion task. Spring defines an SPI (Service Provider Interface) to implement type conversion logic. For your specific problem you can define your Type Conversion logic by implementing Converter interface.
#Component
public class StringToListConverter implements Converter<String, List<String>> {
#Override
public List<String> convert(String source) {
return Arrays.asList(source.split(","));
}
}
You can also convert your request parameter to List<SortPram> according your logic (but I am not sure about your that logic from your question). This is it! Now Spring get known that how to bind your request paramter to a list.
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<MyResponse> participants(#RequestParam("sort") List<String> sort) {
// .. do your logic
}
There are many more ways to define you custom data binder. Check this
A Custom Data Binder in Spring MVC article.
Spring documentation about Validation, Data Binding, and Type Conversion
Yes, you can certainly do that, you're almost there.
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<MyResponse> participants(#RequestParam("sort") List<String> sort) {
//...
}
You should now be able to call your service like this (if search is located at your root, otherwise adapt according to your situation):
curl "localhost:8080/search?sort=sortField1&sort=sortField2&sort=sortField3"
Hope this helps!
EDIT
Sorry, I have read your comments and what you need is clear to me now. I have created a workaround for you that is almost what you want I think.
So first a SortParams class:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class SortParams {
private List<SortParam> sortParamList;
public SortParams(String commaSeparatedString) {
sortParamList = Arrays.stream(commaSeparatedString.split(","))
.map(p -> SortParam.valueOf(p))
.collect(Collectors.toList());
}
public List<SortParam> getSortParamList() {
return this.sortParamList;
}
public enum SortParam {
FOO, BAR, FOOBAR;
}
}
Then your controller could look like this:
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<List<SortParams.SortParam>> search(#RequestParam("sort") SortParams sort) {
return ResponseEntity.ok(sort.getSortParamList());
}
Now your SortParams object has a list of SortParam:
curl "localhost:8080/search?sort=FOO,BAR"
["FOO","BAR"]
Would something like this fit what you're looking for?
It could be helpfull if Kotlin
private const val DELIMITER: Char = ':'
private val DEFAULT_DIRECTION: Sort.Direction = Sort.Direction.ASC
private fun parseFrom(source: String): Sort.Order = if (source.contains(DELIMITER)) {
Sort.Order(Sort.Direction.fromString(source.substringAfter(DELIMITER)), source.substringBefore(DELIMITER))
} else Sort.Order(DEFAULT_DIRECTION, source)
// if few sort paremeters
#Component
class RequestSortConverterArray : Converter<Array<String>, Sort> {
override fun convert(source: Array<String>): Sort? = if (source.isEmpty()) Sort.unsorted()
else source.map { parseFrom(it) }.let { Sort.by(it) }
}
// if single sort paremeter
#Component
class RequestSortConverterString : Converter<String, Sort> {
override fun convert(source: String): Sort? = if (source.isEmpty()) Sort.unsorted()
else Sort.by(parseFrom(source))
}
...
#GetMapping
fun search(
#RequestParam(required = false, defaultValue = "0") page: Int,
#RequestParam(required = false, defaultValue = "20") size: Int,
#RequestParam(required = false, defaultValue = "myfield1:asc") sort: Sort
) {
val pageable = PageRequest.of(page, size, sort)
// make a search by repository
}

Allowed values for RequestParam in get url

Is it possible to set possible values for #RequestParam(value = "id") String id)
?
The ideal is: I give some List<String> allowedValues and it automatically validate it. (This list will be loaded from database).
use an enum instead of a String and define your enum values
#RequestMapping(value = "yourRestEndPoint", method = RequestMethod.GET)
public anyReturnType methodName(#RequestParam(defaultValue = "ONE") IdValue id) {
....
}
and have a separate class defined e.g
public enum IdValue {
ONE, TWO, THREE
}
It is possible but not some magical way but by simple Validation checks.
Eg:
#RequestMapping(value = "yourRestEndPoint", method = RequestMethod.GET)
public anyReturnType methodName(#RequestParam(value = "id") String id){
List<String> allowedValues = getAllowedValuesFromDB();
if(allowedValues.contains(id)){//check if incoming ID belongs to Allowed Values
//Do your action....
}
}
private List<String> getAllowedValuesFromDB(){
List<String> allowedValues = new ArrayList<>();
//fetch list from DB
//set fetched values to above List
return allowedValues;
}
Second Way
If you want to do it like we do Bean Validation, using #Valid, which is for Bean not just a single parameter and also required Validator to be configured, then check out this & this answer.
Hope this helps.

Convert Page<Entity> to PageDTO<EntityDTO>

I am using spring data JPA.
my controller looks like following
#RequestMapping(value = "/pages/{pageNumber}", method = RequestMethod.GET, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Page<User>> paginatedUser(#PathVariable final Integer pageNumber)
{
final PageRequest request = new PageRequest(pageNumber - 1, DEFAULt_PAGE_SIZE, Sort.Direction.DESC, "startTime");
return new ResponseEntity<>(userRepository.findAll(request), HttpStatus.OK);
}
Now i decided to send instead of Page object, a PageDTO object to restrict things from sending.Is there any way i can convert Page to PageDTO using java 8.
I saw Page is derived from Iterable So i guess i can do something like following but not sure how to put it together with PageDTO and UserDTO.
StreamSupport.stream(userRepository.findAll(request).spliterator(),false)
is there any effecient java 8 way to do this.
I came up with this solution
#RequestMapping(value = "/pages/{pageNumber}", method = RequestMethod.GET, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<PageDTO> paginatedUser(#PathVariable final Integer pageNumber)
{
final PageRequest request = new PageRequest(pageNumber - 1, DEFAULt_PAGE_SIZE, Sort.Direction.DESC, "startTime");
final Page<User> page = userRepository.findAll(request);
return new ResponseEntity<>(new PageDTO(page, StreamSupport.stream(page.getContent().spliterator(), true).map(UserDTO::new)
.collect(Collectors.toList())), HttpStatus.OK);
}
public class PageDTO {
private int beginIndex;
private int currentIndex;
private int endIndex;
private List<?> entities;
public PageDTO(final Page<?> page, final List<?> entities) {
this.entities = entities;
this.currentIndex = page.getNumber() + 1;
this.beginIndex = Math.max(1, currentIndex - 5);
this.endIndex = Math.min(beginIndex + 10, page.getTotalPages());
}
Would like to know if there is another effecient way to do this?
I know this is an old question, but I ran into the same problem and I'd like to provide a possible solution to whomever may be interested. Here is what I found that helped me with my code:
https://github.com/pkainulainen/spring-data-jpa-examples/blob/master/query-methods/src/main/java/net/petrikainulainen/springdata/jpa/todo/TodoMapper.java
I also used JpaRepository which paginates the data, so that the DTO page will have the same parameters (page number, size etc). Here is my repository:
#Repository
public interface Repository extends JpaRepository<Entity, Integer> {
/**
* Name the method according to what query you need to execute
* e.g. findAll --> return all the rows that satisfy the following conditions,
* ByUsername --> username is a field in entity class,
* #param pageable: pagination is applied on the data.
* #return
*/
public Page<Entity> findAllByUsername(String username, Pageable pageable);
}
This is the method where I do the mapping:
public Page<EntityDTO> findByUsername(String username, Pageable pageable){
Page<Entity> entityPage = entityRepository.findAllByUsername(username, pageable);
List<EntityDTO> dtos = mapper.entityToEntityDTOs(entityPage.getContent());
return new PageImpl<>(dtos, pageable, entityPage.getTotalElements());
}
And my Mapstruct mapper:
import org.mapstruct.factory.Mappers;
/**
* Mapper for converting entity to DTO.
*/
#Mapper(componentModel = "spring", uses = {})
public interface Mapper {
/**
* The interface declares a member INSTANCE, providing clients access to the mapper implementation,
* which is the file target\generated-sources\com\company\springapp\dto\mappers\MapperImpl.java
* (automatically generated when compiling the project).
*/
AuditMapper INSTANCE = Mappers.getMapper( Mapper.class );
/**
* Convert entity to DTO.
* Mappings are used to 'bind' entity fields to DTO fields (for the mapper's implementation).
* #param entity
* #return
*/
#Mappings({
#Mapping(source = "id", target = "id"),
#Mapping(source = "username", target = "dtoUsername"),
#Mapping(source = "action", target = "dtoAction")
})
public EntityDTO entityToEntityDTO(Entity entity);
/**
* Convert entities' list to DTOs' list.
* #param entities
* #return
*/
public List<EntityDTO> entitiesToEntityDTOs(List<Entity> entities);
}
It's a too late answer, but this solution worked for me
Service
public Page<EntityDto> getAllEntities(Pageable pageable) {
return entityRepository.findAll(pageable).map(entityMapper::toEntityDto);
}
Mapper
import org.mapstruct.Mapper;
#Mapper(componentModel = "spring")
public interface EntityMapper {
EntityDto toEntityDto(Entity entity);
}
Controller
#GetMapping(path = "/entities")
public List<EntityDto> getAllEntities(Pageable pageable) {
Page<EntityDto> page = entityService.getAllEntities(pageable);
return page.getContent();
}
this will return a list of entities with size = 10 from the first page
http://localhost:port/api/entities?page=0&size=10
Thanks to JHipster!

Validate only one spring annotation on form field and independently from others field

i have a specific requirement even if it seems to be pretty straightforward.
I have a form for a user (give you a simplify version of it).
public class Form{
#NotEmpty(message = "{empty}")
#Size(min = 3, max = 32, message = "{size}")
private String firstName;
#NotEmpty(message = "{empty}")
#Size(min = 3, max = 32, message = "{size}")
private String lastName;
}
I want both field to be validate independently, fail at first error (only display one error message then).
For exemple, if i send firstName="" and lastName="aa", i want to get empty message for firstName and size message for lastName.
But by doing that i get 2 error messages for firstName (size and empty).
First alternative i found is using a #groupsequences but still cannot validate field independently.
So i created a new annotation :
#Documented
#Constraint(validatedBy = {})
#Retention(RetentionPolicy.RUNTIME)
#NotEmpty(message = "{empty}", groups = Check.firstTry.class)
#Size(min = 5, max = 32, message = "{size}", groups = Check.secondTry.class)
#GroupSequence({ Check.firstTry.class, Check.secondTry.class, Name.class })
public #interface Name {
public abstract String message() default "{empty}";
public abstract Class<?>[] groups() default {};
public abstract Class<?>[] payload() default {};
}
But it still validating every messages.
If i use #ReportAsSingleViolation it will always return the default message.
Did i miss something somewhere, form validation should be pretty easy for this case, it's not a weird case, is it ?
Thank You
Write a custom ConstraintValidator that checks the constraints one-by-one and reports the first one it finds. The (rather large) downside of this is that you end up reimplementing the checking mechanism for each component annotation, but you do get total control over the error messages.
public class MyConstraintValidator implements ConstraintValidator<Name, String> {
private Name annotation;
#Override
public void initialize(MyConstraintAnnotation annotation) {
this.annotation = annotation;
}
#Override
public boolean isValid(String name, ConstraintValidatorContext context) {
if (name == null) {
context.buildConstraintViolationWithTemplate(emptyMessage).addConstraintViolation();
return false;
} else if (name.length() < getMinSize(annotation)) {
context.buildConstraintViolationWithTemplate(tooShortMessage).addConstraintViolation();
return false;
} else if (name.length > getMaxSize(annotation)) {
context.buildConstraintViolationWithTemplate(tooLongMessage).addConstraintViolation();
return false;
} else {
return true;
}
}
}
Maybe if you could get a handle on the ConstraintValidatorFactory and get the ConstraintValidators you need for each composing annotation? Although that exposes you to the internals of the implementation. It's not a perfect answer at all, but it would get what you need.

Categories

Resources