Interface method that has different parameters in Java - java

Looking for some guidance on designing some code in Java.
Currently I have something like this....
#Service
class SomeService {
#Autowired
private FilterSoldOut filterSoldOut;
#Autowired
private FilterMinPriceThreshold filterMinPriceThreshold;
public List<Product> getProducts() {
List<Product> products = //...code to get some products
// Returns list of in-stock products
products = filterSoldOut.doFilter(products);
// Returns list of products above min price
products = filterMinPriceThreshold.doFilter(minPrice, products);
return products;
}
}
What I would like to be able to do is create a Filter interface with a doFilter method and then in SomeService create a List filters, which is autowired by Spring. Then in the getProducts method I can iterate the filters list and invoke doFilter. This way in the future I can, create new classes that implement the Filter interface and add them to the list via Spring configuration, and have the new filter applied without having to change the code.
But, the problem is that the parameters to the doFilter method can be different. I've read about the Command Pattern, and the Visitor Pattern but they don't quite seem to fit the bill.
Can anyone suggest a good pattern to achieve what I've described?
Thanks.

There are many ways to do this. Some are complicated, some are simpler. The simplest one would be to use varargs or an array of Object elements. The problem here is that you have to cast each objetc to its proper type in order to use them and that can be a little tricky if there are multiple types in an unknown order.
Another option is to use a Map<String,Object> (which you can wrap in a class of your own if required, something lile FilterParams) that stores parameters based on a name, and you can then obtain them and cast them accordingly.
Edit
Considering that the parameters vary on runtime, you'll need someone "well informed" about the current configuration.
Not pattern-wise but I'd rather keep it simple without using too many fancy names. What about introducing a FilterConfigurator that has a simple overloaded method configure that recieves the particular filter and configures it based on its type?. This configurator is the informed entity that knows the current values for those parameters.
The goal is to rid Service from the responsibility of configuring a filter.
In addition, if you create your Filter class, you'll be able to implement a single doFilter that you can invoke without changes.
There's another Idea... and it involves a FilterFactory that creates and initializes filters, thus having a filter 100% configured from scratch. This factory can rely on the very same FilterConfigurer or do it itself.

old:
I'd suggest you setting the filter state at construction time or at
least before you getProducts().
In your example with the two filters one of them is (probably)
checking a database for availability of the product and the other one
is comparing the product's price to some preset value. This value
(minPrice) is known before the filter is applied. It can
also be said that the filter depends on it, or that it's part of the
filter's state. Therefore I'd recommend you putting the
minPrice inside the filter at construction time (or via a
setter) and then only pass the list of products you want to filter.
Use the same pattern for your other filters.
new suggestion (came up with it after the comments):
You can create a single object (AllFiltersState) that holds all the values for all the filters. In your controller set whatever criteria you need in this object (minPrice, color, etc.) and pass it to every filter along the products - doFilter(allFiltersState, products).

As Cris say you can use next function definition:
public List<Product> doFilter(Object...args) {
if (args.length != 2)
throw new IllegalArgumentException();
if (! (args[0] instanceof String))
throw new IllegalArgumentException();
if (! (args[2] instanceof Integer))
throw new IllegalArgumentException();
String stringArgument = (String) args[0];
Integer integerArgument = (Integer) args[1];
// your code here
return ...;
}
or with command pattern:
public interface Command {
}
public class FirstCommand implements Command {
private String string;
// constructor, getters and setters
}
public class SecondCommand implements Command {
private Integer integer;
// constructor, getters and setters
}
// first service function
public List<Product> doFilter(Command command) {
if (command instanceof FirstCommand)
throw new IllegalArgumentException();
FirstCommand firstCommand = (FirstCommand) command;
return ...;
}
// second service function
public List<Product> doFilter(Command command) {
if (command instanceof SecondCommand)
throw new IllegalArgumentException();
SecondCommand secondCommand = (SecondCommand) command;
return ...;
}
EDIT:
Ok, i understand your question. And think you can create various session scoped filters.
#Service
class SomeService {
#Autowired(required = false)
private List<Filter> filters;
public List<Product> getProducts() {
List<Product> products = //...code to get some products
if (filters != null) {
for (Filter filter : filters)
products = filter.doFilter(products);
}
return products;
}
}
And then create filters with settings fields:
public PriceFilter implements Filter {
private Integer minPrice;
private Integer maxPrice;
// getters and setters
public List<Product> doFilter(List<Product> products) {
// implementation here
}
}
public ContentFilter implements Filter {
private String regexp;
// getters and setters
public List<Product> doFilter(List<Product> products) {
// implementation here
}
}
Then user can configure this filters for session and use service function getProducts to get result.

Having a list of filters getting autowired is not a very good approach to solve your problem.
Every filter depends on different types of parameters which would need to be passed to the doFilter method. Needing to do so makes the approach highly unflexible. Yes you could use varargs but it would just create a mess. That's why it's probably easier to implement a builder to build you a chain of filters to be applied to the collection of products. Adding new filters to the builder becomes a trivial task. The Builder Pattern is very useful when a lot of different parameters are at play.
Consider having this interface:
public interface CollectionFilter<T> {
public Collection<T> doFilter(Collection<T> collection);
}
A filter chaining class which applies all filters to the collection:
public class CollectionFilterChain<T> {
private final List<CollectionFilter<T>> filters;
public CollectionFilterChain(List<CollectionFilter<T>> filters) {
this.filters = filters;
}
public Collection<T> doFilter(Collection<T> collection) {
for (CollectionFilter<T> filter : filters) {
collection = filter.doFilter(collection);
}
return collection;
}
}
The two CollectionFilter<T> implementations:
public class InStockFilter<T> implements CollectionFilter<T> {
public Collection<T> doFilter(Collection<T> collection) {
// filter
}
}
public class MinPriceFilter<T> implements CollectionFilter<T> {
private final float minPrice;
public MinPriceFilter(float minPrice) {
this.minPrice = minPrice;
}
public Collection<T> doFilter(Collection<T> collection) {
// filter
}
}
And a builder to let you build the filter chain in a easy way:
public class CollectionFilterChainBuilder<T> {
List<CollectionFilter<T>> filters;
public CollectionFilterChainBuilder() {
filters = new ArrayList<CollectionFilter<T>>();
}
public CollectionFilterChainBuilder<T> inStock() {
filters.add(new InStockFilter<T>());
return this;
}
public CollectionFilterChainBuilder<T> minPrice(float price) {
filters.add(new MinPriceFilter<T>(price));
return this;
}
public CollectionFilterChain<T> build() {
return new CollectionFilterChain<T>(filters);
}
}
With the builder it's easy to create a filter chain as follows:
CollectionFilterChainBuilder<Product> builder =
new CollectionFilterChainBuilder();
CollectionFilterChain<Product> filterChain =
builder.inStock().minPrice(2.0f).build();
Collection<Product> filteredProducts =
filterChain.doFilter(products);
In a more dynamic settings you could use the builder like:
CollectionFilterChainBuilder<Product> builder = new CollectionFilterChainBuilder();
if (filterInStock) {
builder.inStock();
}
if (filterMinPrice) {
builder.minPrice(minPrice);
}
// build some more

Related

Java Spring Security #PostFilter performance

My Java Spring application, which uses ACLs, has a service method to retrieve all the objects corresponding to a given user:
#Override
#PostFilter("hasPermission(filterObject, 'READ') or hasPermission(filterObject, 'ADMINISTRATION')")
public List<SomeClass> findAll() {
return SomeClassRepository.findAll();
}
Unfortunately, where there are many objects in the database, this method takes too much time to complete (over 1 second). Probably because it will first fetch all the objects from the database, and then filter them one by one in memory. How can I optimize this without losing the benefits of Spring ACLs?
Edit: the solution I came up with for now is to create repositories for the acl_sid and acl_entry repositories and to fetch the IDs of the object of interest through those repositories. This gives me a 10x improvement in execution time compared to the method above. The new code looks like this:
#Override
#PostFilter("hasPermission(filterObject, 'READ') or hasPermission(filterObject, 'ADMINISTRATION')")
public List<SomeClass> findAll() {
List<SomeClass> result = new ArrayList<SomeClass>();
Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
AclSid sid = aclSidRepository.findBySid(Long.toString(userId));
List<AclEntry> aclEntries = aclEntryRepository.findBySid(sid);
for (AclEntry aclEntry : aclEntries) {
AclObjectIdentity aclObjectIdentity = aclEntry.getAclObjectIdentity();
AclClass aclClass = aclObjectIdentity.getObjectIdClass();
if (aclClass.getClassName().equals("com.company.app.entity.SomeClass")) {
Optional<SomeClass> SomeClass = SomeClassRepository
.findById(aclObjectIdentity.getObjectIdIdentity());
if (SomeClass.isPresent()) {
result.add(SomeClass.get());
}
}
}
return result;
}
As Spring filters the information in memory the performance will depend on the actual number of results: if there is a large amount of them, I am afraid that perhaps only caching your repository results before filtering the information may be a suitable solution.
To deal with the problem, you can filter the results at the database level. Two approaches come to my mind:
Either use Specifications, and filter the results at the database level taking into account the information about the principal exposed by Spring Security SecurityContext and including the necessary filter Predicates in order to restrict the information returned.
Or, if you are using Hibernate, use entity filters to again, based on the information about the principal exposed by Spring Security, apply the necessary data restrictions. Please, see this related SO question which provides great detail about the solution.
Please, consider for instance the use case of the Spring Data Specifications.
Instead of SomeClass, let's suppose that we are working with bank accounts. Let's create the corresponding entity:
#Entity
public class BankAccount {
#Id
private String accountNumber;
private Float balance;
private String owner;
private String accountingDepartment;
//...
}
And the corresponding repository:
public interface BankAccountRepository extends Repository<BankAccount, String>, JpaSpecificationExecutor<BankAccount, String> {
}
In order to filter the information depending on the user who is performing the query, we can define an utility method that, based on the user permissions, returns a List of Predicates that we can add later to the ones we are using in a certain Specification when filtering the bank accounts:
public static List<Predicate> getPredicatesForRestrictingDataByUser(Root<BankAccount> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// I realized in your edit that you are returning the user id instead of the user object.
// There is nothing wrong with it but you are losing a valuable information: if you provide
// a convenient UserDetails implementation you can have direct access to the authorities a user has, etc
User user = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// Restrict data based on actual permissions
// If the user is an admin, we assume that he/she can see everything, and we will no return any predicates
if (hasAuthority(user, 'ADMINISTRATION')) {
return Collections.emptyList();
}
// Let's introduce the accounting manager role.
// Suppose that an accounting manager can see all the accounts in his/her department
if (hasAuthority(user, 'ACCOUNTING_MANAGER')) {
return Collections.singletonList(cb.equal(root.get(BankAccount_.accountingDeparment), user.getDepartment()))
}
// In any other case, a user can only see the bank account if he/she is the account owner
return Collections.singletonList(cb.equal(root.get(BankAccount_.owner), user.getId()));
}
Where hasAuthority can look like:
public static boolean hasAuthority(User user, String... authorities) {
if (user instanceof UserDetails) {
for (String authority : authorities) {
return authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.findAny(a -> a.equals(authority))
.isPresent();
}
}
return false;
}
Now, use these methods when constructing your Specifications. Consider for instance:
public static Specification<BankAccount> getBankAccounts(final BankAccountFilter filterCriteria) {
return new Specification<BankAccount>() {
#Override
public Predicate toPredicate(Root<BankAccount> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
// Build your predicate list according to the user provided filter criteria
String accountNumber = filterCriteria.getAccountNumber();
if (accountNumber != null) {
predicates.add(cb.equal(root.get(BankAccount_.accountNmber), accountNumber);
}
//...
// And now, restrict the information a user can see
// Ideally, define getPredicatesForRestrictingDataByUser in a generic class more suitable for being reused
List<Predicate> predicatesForRestrictingDataByUser = getPredicatesForRestrictingDataByUser(root, query, cb);
predicates.addAll(predicatesForRestrictingDataByUser);
Predicate predicate = cb.and(predicates.toArray(new Predicate[predicates.size()]));
return predicate;
}
};
}
Please, forgive me for the simple use case, but I hope you get the idea.
The solution proposed in his comment by #OsamaAbdulRehman looks interesting as well, although I honestly never tested it.

Dynamic POJO validation based on groups in spring

Consider the following pojo for reference:
public class User{
private String username;
private String firstName;
private String middleName;
private String lastName;
private String phone;
//getters and setters
}
My application is a basically spring-boot based REST API which exposes two endpoints, one to create the user and the other to retrieve a user.
The "users" fall into certain categories, group-a, group-b etc. which I get from the headers of the post request.
I need to validated the user data in runtime and the validations may differ based on the group of a user.
for example, the users that fall into group-a may have phone numbers as an optional field whereas it might be a mandatory field for some other group.
The regex may also vary based on their groups.
I need to be able to configure spring, to somehow dynamically validate my pojo as soon as they are created and their respective set of validations get triggered based on their groups.
Maybe I can create a yml/xml configuration which would allow me to enable this?
I would prefer to not annotate my private String phone with #NotNull and #Pattern.
My configuration is as follows:
public class NotNullValidator implements Validator {
private String group;
private Object target;
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
#Override
public void validate(Object o) {
if (Objects.nonNull(o)) {
throw new RuntimeException("Target is null");
}
}
}
public interface Validator {
void validate(Object o);
}
#ConfigurationProperties(prefix = "not-null")
#Component
public class NotNullValidators {
List<NotNullValidator> validators;
public List<NotNullValidator> getValidators() {
return validators;
}
public void setValidators(List<NotNullValidator> validators) {
this.validators = validators;
}
}
application.yml
not-null:
validators:
-
group: group-a
target: user.username
-
group: group-b
target: user.phone
I want to configure my application to somehow allow the validators to pick their targets (the actual objects, not the strings mentioned in the yml), and invoke their respective public void validate(Object o) on their targets.
P.S.
Please feel free to edit the question to make it better.
I am using jackson for serializing and deserializing JSON.
The easiest solution to your problem, as i see it, is not with Spring or the POJOs themselves but with a design pattern.
The problem you're describing is easily solved by a strategy pattern solution.
You match the strategy to use by the header you're expecting in the request, that describes the type of user, and then you perform said validations inside the strategy itself.
This will allow you to use the same POJO for the whole approach, and deal with the specifics of handling/parsing and validating data according to the each type of user's strategy.
Here's a link from wiki books with a detailed explanation of the pattern
Strategy Pattern
Suppose you have a basic interface for your strategies:
interface Strategy {
boolean validate(User user);
}
And you have 2 different implementations for the 2 different types of user:
public class StrategyA implements Strategy {
public boolean validate(User user){
return user.getUsername().isEmpty();
}
}
public class StrategyB implements Strategy {
public boolean validate(User user){
return user.getPhone().isEmpty();
}
}
You add a Strategy attribute to your User POJO and assign the right implementation of the Strategy to that attribute when you receive the post request.
Everytime you need to validate data for that user you just have to invoke the validate method of the assigned strategy.
If each User can fit multiple strategies, you can add a List<Strategy> as an attribute instead of a single one.
If you don't want to change the POJO you have to check which is the correct strategy every time you receive a post request.
Besides the validate method you can add methods to handle data, specific to each strategy.
Hope this helps.
You can use validation groups to control which type of user which field gets validated for. For example:
#NotBlank(groups = {GroupB.class})
private String phone;
#NotBlank(groups = {GroupA.class, GroupB.class})
private String username;
Then you use the headers from the request that you mentioned to decide which group to validate against.
See http://blog.codeleak.pl/2014/08/validation-groups-in-spring-mvc.html?m=1 for a complete example.
Updated to include a more comprehensive example:
public class Val {
private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
public boolean isValid(User user, String userType) {
usergroups userGroup = usergroups.valueOf(userType);
Set<ConstraintViolation<User>> constraintViolations = validator.validate(user, userGroup.getValidationClass());
return constraintViolations.isEmpty();
}
public interface GroupA {}
public interface GroupB {}
public enum usergroups {
a(GroupA.class),
b(GroupB.class);
private final Class clazz;
usergroups(Class clazz) {
this.clazz = clazz;
}
public Class getValidationClass() {
return clazz;
}
}
}
This doesn't use application.yaml, instead the mapping of which fields are validated for each group is set in annotations, similar results using Spring's built in validation support.
I was able to solve my problem with the use of Jayway JsonPath.
My solution goes as follows:
Add a filter to your API which has the capability to cache the InputStream of the ServletRequest since it can be read only once. To achieve this, follow this link.
Create a bunch of validators and configure them in your application.yml file with the help of #ConfigurationProperties. To achieve this, follow this link
Create a wrapper which would contain all your validators as a list and initialize it with #ConfigurationProperties and the following configuration:
validators:
regexValidators:
-
target: $.userProfile.lastName
pattern: '[A-Za-z]{0,12}'
group: group-b
minMaxValidators:
-
target: $.userProfile.age
min: 18
max: 50
group: group-b
Call the validate method in this wrapper with the group which comes in the header, and then call the validate of the individual validators. To achieve this, I wrote the following piece of code in my wrapper:
public void validate(String input, String group) {
regexValidators.stream()
.filter(validator -> group.equals(validator.getGroup()))
.forEach(validator -> validator.validate(input));
minMaxValidators.stream()
.filter(validator -> group.equals(validator.getGroup()))
.forEach(validator -> validator.validate(input));
}
and the following method in my validator:
public void validate(String input) {
String data = JsonPath.parse(input).read(target);
if (data == null) {
throw new ValidationException("Target: " + target + " is NULL");
}
Matcher matcher = rule.matcher(data);
if (!matcher.matches()) {
throw new ValidationException("Target: " + target + " does not match the pattern: " + pattern);
}
}
I have created a functioning project to demonstrate the validations and it can be found here.
I understand that the answer alone might not be very clear, please follow the above mentioned url for the complete source code.

QueryDsl web query on the key of a Map field

Overview
Given
Spring Data JPA, Spring Data Rest, QueryDsl
a Meetup entity
with a Map<String,String> properties field
persisted in a MEETUP_PROPERTY table as an #ElementCollection
a MeetupRepository
that extends QueryDslPredicateExecutor<Meetup>
I'd expect
A web query of
GET /api/meetup?properties[aKey]=aValue
to return only Meetups with a property entry that has the specified key and value: aKey=aValue.
However, that's not working for me.
What am I missing?
Tried
Simple Fields
Simple fields work, like name and description:
GET /api/meetup?name=whatever
Collection fields work, like participants:
GET /api/meetup?participants.name=whatever
But not this Map field.
Customize QueryDsl bindings
I've tried customizing the binding by having the repository
extend QuerydslBinderCustomizer<QMeetup>
and overriding the
customize(QuerydslBindings bindings, QMeetup meetup)
method, but while the customize() method is being hit, the binding code inside the lambda is not.
EDIT: Learned that's because QuerydslBindings means of evaluating the query parameter do not let it match up against the pathSpecs map it's internally holding - which has your custom bindings in it.
Some Specifics
Meetup.properties field
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "MEETUP_PROPERTY", joinColumns = #JoinColumn(name = "MEETUP_ID"))
#MapKeyColumn(name = "KEY")
#Column(name = "VALUE", length = 2048)
private Map<String, String> properties = new HashMap<>();
customized querydsl binding
EDIT: See above; turns out, this was doing nothing for my code.
public interface MeetupRepository extends PagingAndSortingRepository<Meetup, Long>,
QueryDslPredicateExecutor<Meetup>,
QuerydslBinderCustomizer<QMeetup> {
#Override
default void customize(QuerydslBindings bindings, QMeetup meetup) {
bindings.bind(meetup.properties).first((path, value) -> {
BooleanBuilder builder = new BooleanBuilder();
for (String key : value.keySet()) {
builder.and(path.containsKey(key).and(path.get(key).eq(value.get(key))));
}
return builder;
});
}
Additional Findings
QuerydslPredicateBuilder.getPredicate() asks QuerydslBindings.getPropertyPath() to try 2 ways to return a path from so it can make a predicate that QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess() can use.
1 is to look in the customized bindings. I don't see any way to express a map query there
2 is to default to Spring's bean paths. Same expression problem there. How do you express a map?
So it looks impossible to get QuerydslPredicateBuilder.getPredicate() to automatically create a predicate.
Fine - I can do it manually, if I can hook into QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()
HOW can I override that class, or replace the bean? It's instantiated and returned as a bean in the RepositoryRestMvcConfiguration.repoRequestArgumentResolver() bean declaration.
I can override that bean by declaring my own repoRequestArgumentResolver bean, but it doesn't get used.
It gets overridden by RepositoryRestMvcConfigurations. I can't force it by setting it #Primary or #Ordered(HIGHEST_PRECEDENCE).
I can force it by explicitly component-scanning RepositoryRestMvcConfiguration.class, but that also messes up Spring Boot's autoconfiguration because it causes
RepositoryRestMvcConfiguration's bean declarations to be processed
before any auto-configuration runs. Among other things, that results in responses that are serialized by Jackson in unwanted ways.
The Question
Well - looks like the support I expected just isn't there.
So the question becomes:
HOW do I correctly override the repoRequestArgumentResolver bean?
BTW - QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver is awkwardly non-public. :/
Replace the Bean
Implement ApplicationContextAware
This is how I replaced the bean in the application context.
It feels a little hacky. I'd love to hear a better way to do this.
#Configuration
public class CustomQuerydslHandlerMethodArgumentResolverConfig implements ApplicationContextAware {
/**
* This class is originally the class that instantiated QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver and placed it into the Spring Application Context
* as a {#link RootResourceInformationHandlerMethodArgumentResolver} by the name of 'repoRequestArgumentResolver'.<br/>
* By injecting this bean, we can let {#link #meetupApiRepoRequestArgumentResolver} delegate as much as possible to the original code in that bean.
*/
private final RepositoryRestMvcConfiguration repositoryRestMvcConfiguration;
#Autowired
public CustomQuerydslHandlerMethodArgumentResolverConfig(RepositoryRestMvcConfiguration repositoryRestMvcConfiguration) {
this.repositoryRestMvcConfiguration = repositoryRestMvcConfiguration;
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((GenericApplicationContext) applicationContext).getBeanFactory();
beanFactory.destroySingleton(REPO_REQUEST_ARGUMENT_RESOLVER_BEAN_NAME);
beanFactory.registerSingleton(REPO_REQUEST_ARGUMENT_RESOLVER_BEAN_NAME,
meetupApiRepoRequestArgumentResolver(applicationContext, repositoryRestMvcConfiguration));
}
/**
* This code is mostly copied from {#link RepositoryRestMvcConfiguration#repoRequestArgumentResolver()}, except the if clause checking if the QueryDsl library is
* present has been removed, since we're counting on it anyway.<br/>
* That means that if that code changes in the future, we're going to need to alter this code... :/
*/
#Bean
public RootResourceInformationHandlerMethodArgumentResolver meetupApiRepoRequestArgumentResolver(ApplicationContext applicationContext,
RepositoryRestMvcConfiguration repositoryRestMvcConfiguration) {
QuerydslBindingsFactory factory = applicationContext.getBean(QuerydslBindingsFactory.class);
QuerydslPredicateBuilder predicateBuilder = new QuerydslPredicateBuilder(repositoryRestMvcConfiguration.defaultConversionService(),
factory.getEntityPathResolver());
return new CustomQuerydslHandlerMethodArgumentResolver(repositoryRestMvcConfiguration.repositories(),
repositoryRestMvcConfiguration.repositoryInvokerFactory(repositoryRestMvcConfiguration.defaultConversionService()),
repositoryRestMvcConfiguration.resourceMetadataHandlerMethodArgumentResolver(),
predicateBuilder, factory);
}
}
Create a Map-searching predicate from http params
Extend RootResourceInformationHandlerMethodArgumentResolver
And these are the snippets of code that create my own Map-searching predicate based on the http query parameters.
Again - would love to know a better way.
The postProcess method calls:
predicate = addCustomMapPredicates(parameterMap, predicate, domainType).getValue();
just before the predicate reference is passed into the QuerydslRepositoryInvokerAdapter constructor and returned.
Here is that addCustomMapPredicates method:
private BooleanBuilder addCustomMapPredicates(MultiValueMap<String, String> parameters, Predicate predicate, Class<?> domainType) {
BooleanBuilder booleanBuilder = new BooleanBuilder();
parameters.keySet()
.stream()
.filter(s -> s.contains("[") && matches(s) && s.endsWith("]"))
.collect(Collectors.toList())
.forEach(paramKey -> {
String property = paramKey.substring(0, paramKey.indexOf("["));
if (ReflectionUtils.findField(domainType, property) == null) {
LOGGER.warn("Skipping predicate matching on [%s]. It is not a known field on domainType %s", property, domainType.getName());
return;
}
String key = paramKey.substring(paramKey.indexOf("[") + 1, paramKey.indexOf("]"));
parameters.get(paramKey).forEach(value -> {
if (!StringUtils.hasLength(value)) {
booleanBuilder.or(matchesProperty(key, null));
} else {
booleanBuilder.or(matchesProperty(key, value));
}
});
});
return booleanBuilder.and(predicate);
}
static boolean matches(String key) {
return PATTERN.matcher(key).matches();
}
And the pattern:
/**
* disallow a . or ] from preceding a [
*/
private static final Pattern PATTERN = Pattern.compile(".*[^.]\\[.*[^\\[]");
I spent a few days looking into how to do this. In the end I just went with manually adding to the predicate. This solution feels simple and elegant.
So you access the map via
GET /api/meetup?properties.aKey=aValue
On the controller I injected the request parameters and the predicate.
public List<Meetup> getMeetupList(#QuerydslPredicate(root = Meetup.class) Predicate predicate,
#RequestParam Map<String, String> allRequestParams,
Pageable page) {
Predicate builder = createPredicateQuery(predicate, allRequestParams);
return meetupRepo.findAll(builder, page);
}
I then just simply parsed the query parameters and added contains
private static final String PREFIX = "properties.";
private BooleanBuilder createPredicateQuery(Predicate predicate, Map<String, String> allRequestParams) {
BooleanBuilder builder = new BooleanBuilder();
builder.and(predicate);
allRequestParams.entrySet().stream()
.filter(e -> e.getKey().startsWith(PREFIX))
.forEach(e -> {
var key = e.getKey().substring(PREFIX.length());
builder.and(QMeetup.meetup.properties.contains(key, e.getValue()));
});
return builder;
}

How to handle conversion of a Set to a List with ModelMapper

I have the following classes:
public class MyEntity {
private Set<MyOtherEntity> other;
}
public class MyDTO {
private List<MyOtherDTO> other;
}
I created two PropertyMaps (using ModelMapper), one for each conversion from and to DTO
public class DTOToEntityPropertyMap extends PropertyMap<MyDTO, MyEntity> {
#Override
protected void configure() {
List<MyOtherDTO> myOtherDTOs = source.getOther();
Set<MyOtherEntity> myOtherEntities = new HashSet<>();
for (MyOtherDTO myOtherDTO : myOtherDTOs) {
MyOtherEntity myOtherEntity = ModelMapperConverterService.convert(myOtherDTO, MyOtherEntity.class);
myOtherEntities.add(myOtherEntity);
}
map().setOther(myOtherEntities);
}
}
public class EntityToDTOPropertyMap extends PropertyMap<MyEntity, MyDTO> {
#Override
protected void configure() {
Set<MyOtherEntity> myOtherEntities = source.getOther();
List<MyOtherDTO> myOtherDTOs = new ArrayList<>();
for (MyOtherEntity myOtherEntity : myOtherEntities) {
MyOtherDTO myOtherDTO = ModelMapperConverterService.convert(myOtherEntity, MyOtherDTO.class);
myOtherDTOs.add(myOtherDTO);
}
map().setOther(myOtherDTOs);
}
}
Adding the PropertyMaps to the ModelMapper creates the following error:
Caused by: org.modelmapper.ConfigurationException: ModelMapper
configuration errors:
1) Invalid source method java.util.List.add(). Ensure that method has
zero parameters and does not return void.
I guess I cannot use List.add() in the configuration of a PropertyMap.
Then, what is the best way to implement the conversion of a List to a Set and backwards in the ModelMapper?
The PropertyMap must use it to map properties. If you put something of different logic it will throw an exception.
So, you have the next options (I put the examples in one direction, the other is the same):
Option 1: Ensure PropertyMap match the properties
Using a property map and ensure ModelMapper will use it, take a look the rules of Matching Strategies:
Standard: by default.
Loose
Strict
Use the one which ensure your property list other will match with the destination property other. For example, if you use Strict Strategy and is not in the correct order it will not match it.
Then, if you are sure the property will match ModelMapper will map the property following its rules for you or if you need it, you would be able to create a PropertyMap whith source MyOtherEntity and destination MyOtherDTO and add it to your ModelMapper instance.
public class DTOToEntityPropertyMap extends PropertyMap<MyOtherEntity, MyOtherDTO> {
#Override
protected void configure() {
map().setPropertyOfOther(source.getPropertyOfOther());
//and so on...
}
}
modelMapper.addMappings(new DTOToEntityPropertyMap());
Note: if your parent mapping (In your case MyEntity to MyDto) have other PropertyMap you would need to add the PropertyMapping of the list classes before the parent, if not it will not use this property map.
Option 2: Create a converter of the lists and use it
Other option is to create a Converter of the classes of your lists (MyOtherEntity -> MyOtherDto) and use it in the parent PropertyMap.
Converter<Set<MyOtherEntity> , List<MyOtherDto>> toOtherDto = new Converter<Set<MyOtherEntity> , List<MyOtherDto>>() {
public String convert(MappingContext<Set<MyOtherEntity> , List<MyOtherDto>> context) {
Set<MyOtherEntity> source = context.getSource();
List<MyOtherDto> destination = context.getDestination();
//Convert it using the logic you want (by hand for example)
return destination;
}
};
Then you must use it in your Parent PropertyMap, as next:
public class EntityToDTOPropertyMap extends PropertyMap<MyEntity, MyDTO> {
Converter<Set<MyOtherEntity> , List<MyOtherDto>> toOtherDto = new Converter<Set<MyOtherEntity> , List<MyOtherDto>>() {
public String convert(MappingContext<Set<MyOtherEntity> , List<MyOtherDto>> context) {
Set<MyOtherEntity> source = context.getSource();
List<MyOtherDto> destination = context.getDestination();
//Convert it using the logic you want (by hand for example)
return destination;
}
};
#Override
protected void configure() {
using(toOtherDto).map(source.getOther()).setOther(null);
}
}
Then ensure this PropertyMap is added to your ModelMapper instance:
modelMapper.addMappings(new EntityToDTOPropertyMap());

Filtering database rows with spring-data-jpa and spring-mvc

I have a spring-mvc project that is using spring-data-jpa for data access. I have a domain object called Travel which I want to allow the end-user to apply a number of filters to it.
For that, I've implemented the following controller:
#Autowired
private TravelRepository travelRep;
#RequestMapping("/search")
public ModelAndView search(
#RequestParam(required= false, defaultValue="") String lastName,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
Page<Travel> travels = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");
mav.addObject("page", page);
mav.addObject("lastName", lastName);
return mav;
}
This works fine: The user has a form with a lastName input box which can be used to filter the Travels.
Beyond lastName, my Travel domain object has a lot more attributes by which I'd like to filter. I think that if these attributes were all strings then I could add them as #RequestParams and add a spring-data-jpa method to query by these. For instance I'd add a method findByLastNameLikeAndFirstNameLikeAndShipNameLike.
However, I don't know how should I do it when I need to filter for foreign keys. So my Travel has a period attribute that is a foreign key to the Period domain object, which I need to have it as a dropdown for the user to select the Period.
What I want to do is when the period is null I want to retrieve all travels filtered by the lastName and when the period is not null I want to retrieve all travels for this period filtered by the lastName.
I know that this can be done if I implement two methods in my repository and use an if to my controller:
public ModelAndView search(
#RequestParam(required= false, defaultValue="") String lastName,
#RequestParam(required= false, defaultValue=null) Period period,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
Page travels = null;
if(period==null) {
travels = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
} else {
travels = travelRep.findByPeriodAndLastNameLike(period,"%"+lastName+"%", pageable);
}
mav.addObject("page", page);
mav.addObject("period", period);
mav.addObject("lastName", lastName);
return mav;
}
Is there a way to do this without using the if ? My Travel has not only the period but also other attributes that need to be filtered using dropdowns !! As you can understand, the complexity would be exponentially increased when I need to use more dropdowns because all the combinations'd need to be considered :(
Update 03/12/13: Continuing from M. Deinum's excelent answer, and after actually implementing it, I'd like to provide some comments for completeness of the question/asnwer:
Instead of implementing JpaSpecificationExecutor you should implement JpaSpecificationExecutor<Travel> to avoid type check warnings.
Please take a look at kostja's excellent answer to this question
Really dynamic JPA CriteriaBuilder
since you will need to implement this if you want to have correct filters.
The best documentation I was able to find for the Criteria API was http://www.ibm.com/developerworks/library/j-typesafejpa/. This is a rather long read but I totally recommend it - after reading it most of my questions for Root and CriteriaBuilder were answered :)
Reusing the Travel object was not possible because it contained various other objects (who also contained other objects) which I needed to search for using Like - instead I used a TravelSearch object that contained the fields I needed to search for.
Update 10/05/15: As per #priyank's request, here's how I implemented the TravelSearch object:
public class TravelSearch {
private String lastName;
private School school;
private Period period;
private String companyName;
private TravelTypeEnum travelType;
private TravelStatusEnum travelStatus;
// Setters + Getters
}
This object was used by TravelSpecification (most of the code is domain specific but I'm leaving it there as an example):
public class TravelSpecification implements Specification<Travel> {
private TravelSearch criteria;
public TravelSpecification(TravelSearch ts) {
criteria= ts;
}
#Override
public Predicate toPredicate(Root<Travel> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
Join<Travel, Candidacy> o = root.join(Travel_.candidacy);
Path<Candidacy> candidacy = root.get(Travel_.candidacy);
Path<Student> student = candidacy.get(Candidacy_.student);
Path<String> lastName = student.get(Student_.lastName);
Path<School> school = student.get(Student_.school);
Path<Period> period = candidacy.get(Candidacy_.period);
Path<TravelStatusEnum> travelStatus = root.get(Travel_.travelStatus);
Path<TravelTypeEnum> travelType = root.get(Travel_.travelType);
Path<Company> company = root.get(Travel_.company);
Path<String> companyName = company.get(Company_.name);
final List<Predicate> predicates = new ArrayList<Predicate>();
if(criteria.getSchool()!=null) {
predicates.add(cb.equal(school, criteria.getSchool()));
}
if(criteria.getCompanyName()!=null) {
predicates.add(cb.like(companyName, "%"+criteria.getCompanyName()+"%"));
}
if(criteria.getPeriod()!=null) {
predicates.add(cb.equal(period, criteria.getPeriod()));
}
if(criteria.getTravelStatus()!=null) {
predicates.add(cb.equal(travelStatus, criteria.getTravelStatus()));
}
if(criteria.getTravelType()!=null) {
predicates.add(cb.equal(travelType, criteria.getTravelType()));
}
if(criteria.getLastName()!=null ) {
predicates.add(cb.like(lastName, "%"+criteria.getLastName()+"%"));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
}
Finally, here's my search method:
#RequestMapping("/search")
public ModelAndView search(
#ModelAttribute TravelSearch travelSearch,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
TravelSpecification tspec = new TravelSpecification(travelSearch);
Page<Travel> travels = travelRep.findAll(tspec, pageable);
PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");
mav.addObject(travelSearch);
mav.addObject("page", page);
mav.addObject("schools", schoolRep.findAll() );
mav.addObject("periods", periodRep.findAll() );
mav.addObject("travelTypes", TravelTypeEnum.values());
mav.addObject("travelStatuses", TravelStatusEnum.values());
return mav;
}
Hope I helped!
For starters you should stop using #RequestParam and put all your search fields in an object (maybe reuse the Travel object for that). Then you have 2 options which you could use to dynamically build a query
Use the JpaSpecificationExecutor and write a Specification
Use the QueryDslPredicateExecutor and use QueryDSL to write a predicate.
Using JpaSpecificationExecutor
First add the JpaSpecificationExecutor to your TravelRepository this will give you a findAll(Specification) method and you can remove your custom finder methods.
public interface TravelRepository extends JpaRepository<Travel, Long>, JpaSpecificationExecutor<Travel> {}
Then you can create a method in your repository which uses a Specification which basically builds the query. See the Spring Data JPA documentation for this.
The only thing you need to do is create a class which implements Specification and which builds the query based on the fields which are available. The query is build using the JPA Criteria API link.
public class TravelSpecification implements Specification<Travel> {
private final Travel criteria;
public TravelSpecification(Travel criteria) {
this.criteria=criteria;
}
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
// create query/predicate here.
}
}
And finally you need to modify your controller to use the new findAll method (I took the liberty to clean it up a little).
#RequestMapping("/search")
public String search(#ModelAttribute Travel search, Pageable pageable, Model model) {
Specification<Travel> spec = new TravelSpecification(search);
Page<Travel> travels = travelRep.findAll(spec, pageable);
model.addObject("page", new PageWrapper(travels, "/search"));
return "travels/list";
}
Using QueryDslPredicateExecutor
First add the QueryDslPredicateExecutor to your TravelRepository this will give you a findAll(Predicate) method and you can remove your custom finder methods.
public interface TravelRepository extends JpaRepository<Travel, Long>, QueryDslPredicateExecutor<Travel> {}
Next you would implement a service method which would use the Travel object to build a predicate using QueryDSL.
#Service
#Transactional
public class TravelService {
private final TravelRepository travels;
public TravelService(TravelRepository travels) {
this.travels=travels;
}
public Iterable<Travel> search(Travel criteria) {
BooleanExpression predicate = QTravel.travel...
return travels.findAll(predicate);
}
}
See also this bog post.

Categories

Resources