Browse Source
QuerydslBindingsFactory creates a completely build up and customized QuerydslBindings instance for a given QuerydslBinderCustomizer type and domain type. This basically involves creating or obtaining an instance of the customizer with the following algorithm: 1. If an explicit customizer type is given and no BeanFactory is configured, we'll simply instantiate the configured type. This allows simple customizer implementations with a no-arg constructor. 2. If a BeanFactory is configured and a customizer type configured, we're trying to locate a bean of that type in the BeanFactory, falling back to creating an instance of the customizer through the BeanFactory if no existing bean definition can be found. This allows unique customizer bean definitions or complex prototype scoped customizer beans created on the fly. 3. If no customizer type is configured, we check whether the the repository of the domain type implements QuerydslBinderCustomizer and use it if so. The extraction of that functionality allows that algorithm being used by Spring Data REST, although only step 3 is currently used there as no explicit customizer can currently be configured with Spring Data REST.pull/136/head
5 changed files with 358 additions and 208 deletions
@ -0,0 +1,176 @@
@@ -0,0 +1,176 @@
|
||||
/* |
||||
* Copyright 2015 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.querydsl.binding; |
||||
|
||||
import java.util.Map; |
||||
|
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.beans.BeansException; |
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException; |
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.ApplicationContextAware; |
||||
import org.springframework.data.querydsl.EntityPathResolver; |
||||
import org.springframework.data.repository.support.Repositories; |
||||
import org.springframework.data.util.TypeInformation; |
||||
import org.springframework.data.web.querydsl.QuerydslPredicate; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ConcurrentReferenceHashMap; |
||||
|
||||
import com.mysema.query.types.EntityPath; |
||||
|
||||
/** |
||||
* @author Oliver Gierke |
||||
*/ |
||||
public class QuerydslBindingsFactory implements ApplicationContextAware { |
||||
|
||||
private static final String INVALID_DOMAIN_TYPE = "Unable to find Querydsl root type for detected domain type %s! User @%s's root attribute to define the domain type manually!"; |
||||
|
||||
private final EntityPathResolver entityPathResolver; |
||||
private final Map<TypeInformation<?>, EntityPath<?>> entityPaths; |
||||
|
||||
private AutowireCapableBeanFactory beanFactory; |
||||
private Repositories repositories; |
||||
|
||||
/** |
||||
* @param entityPathResolver must not be {@literal null}. |
||||
*/ |
||||
public QuerydslBindingsFactory(EntityPathResolver entityPathResolver) { |
||||
|
||||
Assert.notNull(entityPathResolver, "EntityPathResolver must not be null!"); |
||||
|
||||
this.entityPathResolver = entityPathResolver; |
||||
this.entityPaths = new ConcurrentReferenceHashMap<TypeInformation<?>, EntityPath<?>>(); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) |
||||
*/ |
||||
@Override |
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { |
||||
this.beanFactory = applicationContext.getAutowireCapableBeanFactory(); |
||||
this.repositories = new Repositories(applicationContext); |
||||
} |
||||
|
||||
/** |
||||
* Returns the {@link EntityPathResolver} used by the factory. |
||||
* |
||||
* @return the entityPathResolver |
||||
*/ |
||||
public EntityPathResolver getEntityPathResolver() { |
||||
return entityPathResolver; |
||||
} |
||||
|
||||
public QuerydslBindings createBindingsFor(Class<? extends QuerydslBinderCustomizer> customizer, |
||||
TypeInformation<?> domainType) { |
||||
|
||||
EntityPath<?> path = verifyEntityPathPresent(domainType); |
||||
|
||||
QuerydslBindings bindings = new QuerydslBindings(); |
||||
findCustomizerForDomainType(customizer, domainType.getType()).customize(bindings, path); |
||||
|
||||
return bindings; |
||||
} |
||||
|
||||
/** |
||||
* Tries to detect a Querydsl query type for the given domain type candidate via the configured |
||||
* {@link EntityPathResolver}. |
||||
* |
||||
* @param candidate must not be {@literal null}. |
||||
* @throws IllegalStateException to indicate the query type can't be found and manual configuration is necessary. |
||||
*/ |
||||
private EntityPath<?> verifyEntityPathPresent(TypeInformation<?> candidate) { |
||||
|
||||
EntityPath<?> path = entityPaths.get(candidate); |
||||
|
||||
if (path != null) { |
||||
return path; |
||||
} |
||||
|
||||
Class<?> type = candidate.getType(); |
||||
|
||||
try { |
||||
path = entityPathResolver.createPath(type); |
||||
} catch (IllegalArgumentException o_O) { |
||||
throw new IllegalStateException( |
||||
String.format(INVALID_DOMAIN_TYPE, candidate.getType(), QuerydslPredicate.class.getSimpleName()), o_O); |
||||
} |
||||
|
||||
entityPaths.put(candidate, path); |
||||
return path; |
||||
} |
||||
|
||||
/** |
||||
* Obtains the {@link QuerydslBinderCustomizer} for the given domain type. Will inspect the given annotation for a |
||||
* dedicatedly configured one or consider the domain types's repository. |
||||
* |
||||
* @param annotation |
||||
* @param domainType |
||||
* @return |
||||
*/ |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
private QuerydslBinderCustomizer<EntityPath<?>> findCustomizerForDomainType( |
||||
Class<? extends QuerydslBinderCustomizer> customizer, Class<?> domainType) { |
||||
|
||||
if (customizer != null && !QuerydslBinderCustomizer.class.equals(customizer)) { |
||||
return createQuerydslBinderCustomizer(customizer); |
||||
} |
||||
|
||||
if (repositories != null && repositories.hasRepositoryFor(domainType)) { |
||||
|
||||
Object repository = repositories.getRepositoryFor(domainType); |
||||
|
||||
if (repository instanceof QuerydslBinderCustomizer) { |
||||
return (QuerydslBinderCustomizer<EntityPath<?>>) repository; |
||||
} |
||||
} |
||||
|
||||
return NoOpCustomizer.INSTANCE; |
||||
} |
||||
|
||||
/** |
||||
* Obtains a {@link QuerydslBinderCustomizer} for the given type. Will try to obtain a bean from the |
||||
* {@link org.springframework.beans.factory.BeanFactory} first or fall back to create a fresh instance through the |
||||
* {@link org.springframework.beans.factory.BeanFactory} or finally falling back to a plain instantiation if no |
||||
* {@link org.springframework.beans.factory.BeanFactory} is present. |
||||
* |
||||
* @param type must not be {@literal null}. |
||||
* @return |
||||
*/ |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
private QuerydslBinderCustomizer<EntityPath<?>> createQuerydslBinderCustomizer( |
||||
Class<? extends QuerydslBinderCustomizer> type) { |
||||
|
||||
if (beanFactory == null) { |
||||
return BeanUtils.instantiateClass(type); |
||||
} |
||||
|
||||
try { |
||||
return beanFactory.getBean(type); |
||||
} catch (NoSuchBeanDefinitionException e) { |
||||
return beanFactory.createBean(type); |
||||
} |
||||
} |
||||
|
||||
private static enum NoOpCustomizer implements QuerydslBinderCustomizer<EntityPath<?>> { |
||||
|
||||
INSTANCE; |
||||
|
||||
@Override |
||||
public void customize(QuerydslBindings bindings, EntityPath<?> root) {} |
||||
} |
||||
} |
||||
@ -0,0 +1,158 @@
@@ -0,0 +1,158 @@
|
||||
/* |
||||
* Copyright 2015 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.querydsl.binding; |
||||
|
||||
import static org.hamcrest.Matchers.*; |
||||
import static org.junit.Assert.*; |
||||
import static org.mockito.Mockito.*; |
||||
|
||||
import java.util.Collections; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Rule; |
||||
import org.junit.Test; |
||||
import org.junit.rules.ExpectedException; |
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory; |
||||
import org.springframework.data.mapping.PropertyPath; |
||||
import org.springframework.data.querydsl.QUser; |
||||
import org.springframework.data.querydsl.SimpleEntityPathResolver; |
||||
import org.springframework.data.querydsl.User; |
||||
import org.springframework.data.repository.support.Repositories; |
||||
import org.springframework.data.util.ClassTypeInformation; |
||||
import org.springframework.data.util.TypeInformation; |
||||
import org.springframework.data.web.querydsl.QuerydslPredicate; |
||||
import org.springframework.test.util.ReflectionTestUtils; |
||||
import org.springframework.web.servlet.ModelAndView; |
||||
|
||||
import com.mysema.query.types.Path; |
||||
import com.mysema.query.types.Predicate; |
||||
import com.mysema.query.types.path.StringPath; |
||||
|
||||
/** |
||||
* Unit tests for {@link QuerydslBindingsFactory}. |
||||
* |
||||
* @author Oliver Gierke |
||||
* @soundtrack Miles Davis - All Blues (Kind of Blue) |
||||
*/ |
||||
public class QuerydslBindingsFactoryUnitTests { |
||||
|
||||
static final TypeInformation<?> USER_TYPE = ClassTypeInformation.from(User.class); |
||||
|
||||
public @Rule ExpectedException exception = ExpectedException.none(); |
||||
|
||||
QuerydslBindingsFactory factory; |
||||
|
||||
@Before |
||||
public void setUp() { |
||||
this.factory = new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE); |
||||
} |
||||
|
||||
/** |
||||
* @see DATACMNS-669 |
||||
*/ |
||||
@Test |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
public void createBindingsShouldHonorQuerydslBinderCustomizerHookWhenPresent() { |
||||
|
||||
Repositories repositories = mock(Repositories.class); |
||||
|
||||
when(repositories.hasRepositoryFor(User.class)).thenReturn(true); |
||||
when(repositories.getRepositoryFor(User.class)).thenReturn(new SampleRepo()); |
||||
|
||||
QuerydslBindingsFactory factory = new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE); |
||||
ReflectionTestUtils.setField(factory, "repositories", repositories); |
||||
|
||||
QuerydslBindings bindings = factory.createBindingsFor(null, USER_TYPE); |
||||
MultiValueBinding<Path<Object>, Object> binding = bindings |
||||
.getBindingForPath(PropertyPath.from("firstname", User.class)); |
||||
|
||||
assertThat(binding.bind((Path) QUser.user.firstname, Collections.singleton("rand")), |
||||
is((Predicate) QUser.user.firstname.contains("rand"))); |
||||
} |
||||
|
||||
/** |
||||
* @see DATACMNS-669 |
||||
*/ |
||||
@Test |
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) |
||||
public void shouldReuseExistingQuerydslBinderCustomizer() { |
||||
|
||||
AutowireCapableBeanFactory beanFactory = mock(AutowireCapableBeanFactory.class); |
||||
when(beanFactory.getBean(SpecificBinding.class)).thenReturn(new SpecificBinding()); |
||||
|
||||
QuerydslBindingsFactory factory = new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE); |
||||
ReflectionTestUtils.setField(factory, "beanFactory", beanFactory); |
||||
|
||||
QuerydslBindings bindings = factory.createBindingsFor(SpecificBinding.class, USER_TYPE); |
||||
MultiValueBinding<Path<Object>, Object> binding = bindings |
||||
.getBindingForPath(PropertyPath.from("firstname", User.class)); |
||||
|
||||
assertThat(binding.bind((Path) QUser.user.firstname, Collections.singleton("rand")), |
||||
is((Predicate) QUser.user.firstname.eq("RAND"))); |
||||
} |
||||
|
||||
/** |
||||
* @see DATACMNS-669 |
||||
*/ |
||||
@Test |
||||
public void rejectsPredicateResolutionIfDomainTypeCantBeAutoDetected() { |
||||
|
||||
exception.expect(IllegalStateException.class); |
||||
exception.expectMessage(QuerydslPredicate.class.getSimpleName()); |
||||
exception.expectMessage("root"); |
||||
|
||||
factory.createBindingsFor(null, ClassTypeInformation.from(ModelAndView.class)); |
||||
} |
||||
|
||||
static class SpecificBinding implements QuerydslBinderCustomizer<QUser> { |
||||
|
||||
public void customize(QuerydslBindings bindings, QUser user) { |
||||
|
||||
bindings.bind(user.firstname).first(new SingleValueBinding<StringPath, String>() { |
||||
|
||||
@Override |
||||
public Predicate bind(StringPath path, String value) { |
||||
return path.eq(value.toUpperCase()); |
||||
} |
||||
}); |
||||
|
||||
bindings.bind(user.lastname).first(new SingleValueBinding<StringPath, String>() { |
||||
|
||||
@Override |
||||
public Predicate bind(StringPath path, String value) { |
||||
return path.toLowerCase().eq(value); |
||||
} |
||||
}); |
||||
|
||||
bindings.excluding(user.address); |
||||
} |
||||
} |
||||
|
||||
public static class SampleRepo implements QuerydslBinderCustomizer<QUser> { |
||||
|
||||
@Override |
||||
public void customize(QuerydslBindings bindings, QUser user) { |
||||
|
||||
bindings.bind(QUser.user.firstname).first(new SingleValueBinding<StringPath, String>() { |
||||
|
||||
@Override |
||||
public Predicate bind(StringPath path, String value) { |
||||
return path.contains(value); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue