DATADOC-34 - Infrastructure for callbacks on RepositoryQuery creation.

Introduced QueryCreationListener that allows plugging in functionality after a query was created.

Heavily refactored QueryMethod and RepositoryQuery subsystem. Splitted up simple EntityMetadata (domain class and potentially other stuff) from the more advanced methods like getId(Object) and isNew(…). QueryMethod now carries a type enum that allows switching over it to determine the execution. Beyond that QueryMethod now returns a EntityMetadata rather than a plain class. 

RepositoryQuery in turn exposes the query method. PartTree allows iterating over all contained Parts as well now. Introduced AbstractEntityInformation that regards an entity as new if its id is null. Simplified handling of RepositoryProxyPostProcessors and aligned them to QueryCreationListener handling.
This commit is contained in:
Oliver Gierke
2011-02-27 18:33:34 +01:00
parent fab26f538f
commit e157f380bb
15 changed files with 367 additions and 94 deletions

View File

@@ -111,6 +111,24 @@ public class Sort implements
}
/**
* Returns the order registered for the given property.
*
* @param property
* @return
*/
public Order getOrderFor(String property) {
for (Order order : this) {
if (order.getProperty().equals(property)) {
return order;
}
}
return null;
}
/*
* (non-Javadoc)
*

View File

@@ -23,6 +23,8 @@ import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.support.EntityMetadata;
import org.springframework.data.repository.util.ClassUtils;
import org.springframework.util.Assert;
@@ -35,6 +37,11 @@ import org.springframework.util.Assert;
*/
public class QueryMethod {
public static enum Type {
SINGLE_ENTITY, PAGING, COLLECTION, MODIFYING;
}
private final Method method;
private final Parameters parameters;
@@ -82,27 +89,21 @@ public class QueryMethod {
}
/**
* Returns whether the given
*
* @param number
* @return
*/
public boolean isCorrectNumberOfParameters(int number) {
public EntityMetadata<?> getEntityMetadata() {
return number == parameters.getBindableParameters()
.getNumberOfParameters();
return new EntityMetadata() {
public Class<?> getJavaType() {
return getDomainClass();
}
};
}
/**
* Returns the domain class for this method.
*
* @return
*/
public Class<?> getDomainClass() {
protected Class<?> getDomainClass() {
return getReturnedDomainClass(method);
return ClassUtils.getReturnedDomainClass(method);
}
@@ -112,7 +113,7 @@ public class QueryMethod {
*
* @return
*/
public boolean isCollectionQuery() {
protected boolean isCollectionQuery() {
Class<?> returnType = method.getReturnType();
return org.springframework.util.ClassUtils.isAssignable(List.class,
@@ -125,7 +126,7 @@ public class QueryMethod {
*
* @return
*/
public boolean isPageQuery() {
protected boolean isPageQuery() {
Class<?> returnType = method.getReturnType();
return org.springframework.util.ClassUtils.isAssignable(Page.class,
@@ -133,6 +134,30 @@ public class QueryMethod {
}
public Type getType() {
if (isModifyingQuery()) {
return Type.MODIFYING;
}
if (isPageQuery()) {
return Type.PAGING;
}
if (isCollectionQuery()) {
return Type.COLLECTION;
}
return Type.SINGLE_ENTITY;
}
protected boolean isModifyingQuery() {
return false;
}
/**
* Returns the {@link Parameters} wrapper to gain additional information
* about {@link Method} parameters.

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.repository.query;
/**
* Interface for a query abstraction.
*
@@ -30,4 +32,12 @@ public interface RepositoryQuery {
* @return
*/
public Object execute(Object[] parameters);
/**
* Returns the
*
* @return
*/
public QueryMethod getQueryMethod();
}

View File

@@ -125,6 +125,26 @@ public class PartTree implements Iterable<OrPart> {
}
/**
* Returns an {@link Iterable} of all parts contained in the
* {@link PartTree}.
*
* @return
*/
public Iterable<Part> getParts() {
List<Part> result = new ArrayList<Part>();
for (OrPart orPart : this) {
for (Part part : orPart) {
result.add(part);
}
}
return result;
}
/**
* Splits the given text at the given keywords. Expects camelcase style to
* only match concrete keywords and not derivatives of it.

View File

@@ -19,22 +19,24 @@ import org.springframework.util.Assert;
/**
* Base class for implementations of {@link EntityMetadata}. Considers an entity
* to be new whenever {@link #getId(Object)} returns {@literal null}.
* Base class for implementations of {@link EntityInformation}. Considers an
* entity to be new whenever {@link #getId(Object)} returns {@literal null}.
*
* @author Oliver Gierke
*/
public abstract class AbstractEntityMetadata<T> implements EntityMetadata<T> {
public abstract class AbstractEntityInformation<T> implements
EntityInformation<T> {
private final Class<T> domainClass;
/**
* Creates a new {@link AbstractEntityMetadata} from the given domain class.
* Creates a new {@link AbstractEntityInformation} from the given domain
* class.
*
* @param domainClass
*/
public AbstractEntityMetadata(Class<T> domainClass) {
public AbstractEntityInformation(Class<T> domainClass) {
Assert.notNull(domainClass);
this.domainClass = domainClass;

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2011 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.repository.support;
/**
* Extension of {@link EntityMetadata} to add functionality to query information
* of entity instances.
*
* @author Oliver Gierke
*/
public interface EntityInformation<T> extends EntityMetadata<T> {
/**
* Returns whether the given entity is considered to be new.
*
* @param entity must never be {@literal null}
* @return
*/
boolean isNew(T entity);
/**
* Returns the id of the given entity.
*
* @param entity must never be {@literal null}
* @return
*/
Object getId(T entity);
}

View File

@@ -22,24 +22,6 @@ package org.springframework.data.repository.support;
*/
public interface EntityMetadata<T> {
/**
* Returns whether the given entity is considered to be new.
*
* @param entity must never be {@literal null}
* @return
*/
boolean isNew(T entity);
/**
* Returns the id of the given entity.
*
* @param entity must never be {@literal null}
* @return
*/
Object getId(T entity);
/**
* Returns the actual domain class type.
*

View File

@@ -26,15 +26,15 @@ import org.springframework.data.domain.Persistable;
* @author Oliver Gierke
*/
@SuppressWarnings("rawtypes")
public class PersistableEntityMetadata<T extends Persistable> extends
AbstractEntityMetadata<T> {
public class PersistableEntityInformation<T extends Persistable> extends
AbstractEntityInformation<T> {
/**
* Creates a new {@link PersistableEntityMetadata}.
* Creates a new {@link PersistableEntityInformation}.
*
* @param domainClass
*/
public PersistableEntityMetadata(Class<T> domainClass) {
public PersistableEntityInformation(Class<T> domainClass) {
super(domainClass);
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2011 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.repository.support;
import org.springframework.data.repository.query.RepositoryQuery;
/**
* Callback for listeners that want to execute functionality on
* {@link RepositoryQuery} creation.
*
* @author Oliver Gierke
*/
public interface QueryCreationListener<T extends RepositoryQuery> {
/**
* Will be invoked just after the {@link RepositoryQuery} was created.
*
* @param query
*/
void onCreation(T query);
}

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.data.repository.support;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Required;
@@ -124,23 +121,6 @@ public abstract class RepositoryFactoryBeanSupport<T extends Repository<?, ?>>
this.factory = createRepositoryFactory();
this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey);
for (RepositoryProxyPostProcessor processor : getRepositoryPostProcessors()) {
this.factory.addRepositoryProxyPostProcessor(processor);
}
}
/**
* Returns all {@link RepositoryProxyPostProcessor} to be added to the
* repository factory to be created. Default implementation will return an
* empty list.
*
* @return
*/
protected List<RepositoryProxyPostProcessor> getRepositoryPostProcessors() {
return Collections.emptyList();
}

View File

@@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.core.GenericTypeResolver;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
@@ -49,6 +50,8 @@ public abstract class RepositoryFactorySupport {
private final List<RepositoryProxyPostProcessor> postProcessors =
new ArrayList<RepositoryProxyPostProcessor>();
private QueryLookupStrategy.Key queryLookupStrategyKey;
private List<QueryCreationListener<?>> queryPostProcessors =
new ArrayList<QueryCreationListener<?>>();
/**
@@ -62,6 +65,20 @@ public abstract class RepositoryFactorySupport {
}
/**
* Adds a {@link QueryCreationListener} to the factory to plug in
* functionality triggered right after creation of {@link RepositoryQuery}
* instances.
*
* @param listener
*/
public void addQueryCreationListener(QueryCreationListener<?> listener) {
Assert.notNull(listener);
this.queryPostProcessors.add(listener);
}
/**
* Adds {@link RepositoryProxyPostProcessor}s to the factory to allow
* manipulation of the {@link ProxyFactory} before the proxy gets created.
@@ -71,7 +88,7 @@ public abstract class RepositoryFactorySupport {
*
* @param processor
*/
protected void addRepositoryProxyPostProcessor(
public void addRepositoryProxyPostProcessor(
RepositoryProxyPostProcessor processor) {
Assert.notNull(processor);
@@ -205,10 +222,10 @@ public abstract class RepositoryFactorySupport {
* interface methods.
*/
public QueryExecuterMethodInterceptor(
RepositoryMetadata repositoryInterface,
RepositoryMetadata repositoryMetadata,
Object customImplementation, Object target) {
this.metadata = repositoryInterface;
this.metadata = repositoryMetadata;
this.customImplementation = customImplementation;
this.target = target;
@@ -216,10 +233,28 @@ public abstract class RepositoryFactorySupport {
getQueryLookupStrategy(queryLookupStrategyKey);
for (Method method : metadata.getQueryMethods()) {
queries.put(
method,
RepositoryQuery query =
lookupStrategy.resolveQuery(method,
repositoryInterface.getDomainClass()));
repositoryMetadata.getDomainClass());
invokeListeners(query, metadata);
queries.put(method, query);
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void invokeListeners(RepositoryQuery query,
RepositoryMetadata metadata) {
for (QueryCreationListener listener : queryPostProcessors) {
Class<?> typeArgument =
GenericTypeResolver.resolveTypeArgument(
listener.getClass(),
QueryCreationListener.class);
if (typeArgument != null
&& typeArgument.isAssignableFrom(query.getClass())) {
listener.onCreation(query);
}
}
}

View File

@@ -15,9 +15,6 @@
*/
package org.springframework.data.repository.support;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ListableBeanFactory;
@@ -59,20 +56,32 @@ public abstract class TransactionalRepositoryFactoryBeanSupport<T extends Reposi
}
/*
* (non-Javadoc)
/**
* Delegates {@link RepositoryFactorySupport} creation to
* {@link #doCreateRepositoryFactory()} and applies the
* {@link TransactionalRepositoryProxyPostProcessor} to the created
* instance.
*
* @see
* org.springframework.data.repository.support.RepositoryFactoryBeanSupport
* #getRepositoryPostProcessors()
* @see org.springframework.data.repository.support.RepositoryFactoryBeanSupport
* #createRepositoryFactory()
*/
@Override
public List<RepositoryProxyPostProcessor> getRepositoryPostProcessors() {
protected final RepositoryFactorySupport createRepositoryFactory() {
return Arrays.asList(txPostProcessor);
RepositoryFactorySupport factory = doCreateRepositoryFactory();
factory.addRepositoryProxyPostProcessor(txPostProcessor);
return factory;
}
/**
* Creates the actual {@link RepositoryFactorySupport} instance.
*
* @return
*/
protected abstract RepositoryFactorySupport doCreateRepositoryFactory();
/*
* (non-Javadoc)
*

View File

@@ -22,32 +22,32 @@ import org.junit.Test;
/**
* Unit tests for {@link AbstractEntityMetadata}.
* Unit tests for {@link AbstractEntityInformation}.
*
* @author Oliver Gierke
*/
public class AbstractEntityMetadataUnitTests {
public class AbstractEntityInformationUnitTests {
@Test(expected = IllegalArgumentException.class)
public void rejectsNullDomainClass() throws Exception {
new DummyAbstractEntityMetadata(null);
new DummyAbstractEntityInformation(null);
}
@Test
public void considersEntityNewIfGetIdReturnsNull() throws Exception {
EntityMetadata<Object> metadata =
new DummyAbstractEntityMetadata(Object.class);
EntityInformation<Object> metadata =
new DummyAbstractEntityInformation(Object.class);
assertThat(metadata.isNew(null), is(true));
assertThat(metadata.isNew(new Object()), is(false));
}
private static class DummyAbstractEntityMetadata extends
AbstractEntityMetadata<Object> {
private static class DummyAbstractEntityInformation extends
AbstractEntityInformation<Object> {
public DummyAbstractEntityMetadata(Class<Object> domainClass) {
public DummyAbstractEntityInformation(Class<Object> domainClass) {
super(domainClass);
}

View File

@@ -32,11 +32,11 @@ import org.springframework.data.domain.Persistable;
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class PersistableEntityMetadataUnitTests {
public class PersistableEntityInformationUnitTests {
@SuppressWarnings("rawtypes")
static final PersistableEntityMetadata<Persistable> metadata =
new PersistableEntityMetadata<Persistable>(Persistable.class);
static final PersistableEntityInformation<Persistable> metadata =
new PersistableEntityInformation<Persistable>(Persistable.class);
@Mock
Persistable<Long> persistable;
@@ -64,8 +64,8 @@ public class PersistableEntityMetadataUnitTests {
@Test
public void returnsGivenClassAsEntityType() throws Exception {
PersistableEntityMetadata<PersistableEntity> info =
new PersistableEntityMetadata<PersistableEntity>(
PersistableEntityInformation<PersistableEntity> info =
new PersistableEntityInformation<PersistableEntity>(
PersistableEntity.class);
assertEquals(PersistableEntity.class, info.getJavaType());

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2011 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.repository.support;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import java.io.Serializable;
import java.lang.reflect.Method;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.RepositoryQuery;
/**
* Unit tests for {@link RepositoryFactorySupport}.
*
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class RepositoryFactorySupportUnitTests {
RepositoryFactorySupport factory = new DummyRepositoryFactory();
@Mock
MyQueryCreationListener listener;
@Mock
PlainQueryCreationListener otherListener;
@Test
public void invokesCustomQueryCreationListenerForSpecialRepositoryQueryOnly()
throws Exception {
factory.addQueryCreationListener(listener);
factory.addQueryCreationListener(otherListener);
factory.getRepository(ObjectRepository.class);
verify(listener, times(1)).onCreation(any(MyRepositoryQuery.class));
verify(otherListener, times(2)).onCreation(any(RepositoryQuery.class));
}
class DummyRepositoryFactory extends RepositoryFactorySupport {
@Override
protected Object getTargetRepository(RepositoryMetadata metadata) {
return new Object();
}
@Override
protected Class<?> getRepositoryBaseClass(Class<?> repositoryInterface) {
return Object.class;
}
@Override
protected QueryLookupStrategy getQueryLookupStrategy(Key key) {
MyRepositoryQuery queryOne = mock(MyRepositoryQuery.class);
RepositoryQuery queryTwo = mock(RepositoryQuery.class);
QueryLookupStrategy strategy = mock(QueryLookupStrategy.class);
when(strategy.resolveQuery(any(Method.class), any(Class.class)))
.thenReturn(queryOne, queryTwo);
return strategy;
}
}
interface ObjectRepository extends Repository<Object, Serializable> {
Object findByClass(Class<?> clazz);
Object findByFoo();
}
interface PlainQueryCreationListener extends
QueryCreationListener<RepositoryQuery> {
}
interface MyQueryCreationListener extends
QueryCreationListener<MyRepositoryQuery> {
}
interface MyRepositoryQuery extends RepositoryQuery {
}
}