QueryDsl web query on the key of a Map field
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.
@Configurationpublic 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;}