DATACMNS-293 - Basic infrastructure for query augmentation.

Added SPI to support generic query augmentation. Implementors implement a QueryAugmentor that can opt into augmentation and will get called for query executions and updates to potentially massage the query to be executed, manipulate or even replace the execution of updates.

We expose a QueryAugmentationEngineAware injection interface for repository base class implementations to expose they want to take part in the query augmentation.

TODO:
- Add BeanPostProcessor to inject QueryAugmentationEngines into QueryAugmentationEngineAware classes
- Automatically pick up QueryAugmentor instances from the application context
- Ordering of QueryAugmentors
- More tests
This commit is contained in:
Oliver Gierke
2013-03-03 15:29:24 +01:00
committed by Oliver Gierke
parent 01f2c30b1d
commit 2550c15166
15 changed files with 1198 additions and 8 deletions

11
pom.xml
View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml version="1.0" encoding="UTF-8"standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>1.12.0.BUILD-SNAPSHOT</version>
<version>1.12.0.DATACMNS-293-SNAPSHOT</version>
<name>Spring Data Core</name>
@@ -131,7 +131,7 @@
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
@@ -199,7 +199,6 @@
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
@@ -217,7 +216,6 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
@@ -251,7 +249,6 @@
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2013 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;
/**
* Interface to abstract value objects that represent the deleted state of an entity. SPI interface that allows
* implementing a deletion strategy over muliple levels, e.g. active, trashed, eventually soft deleted.
*
* @since 1.6
* @author Oliver Gierke
*/
public interface DeleteStates<T> {
/**
* Returns the value that represents the active state.
*
* @return
*/
T activeValue();
/**
* Return the next deleted value. This represents a state change to the next more deleted state, e.g. from active to
* trashed, from trashed to eventually deleted etc.
*
* @return
*/
T delete();
/**
* Return the value the restored object shall carry. This represents a state change to the next less deleted state,
* e.g. from eventually deleted to trashed, from trashed to active etc.
*
* @return
*/
T restore();
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2013 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;
/**
* Enum based implementation of {@link DeleteStates}.
*
* @since 1.6
* @author Oliver Gierke
*/
public enum Deleted implements DeleteStates<Deleted> {
/**
* The object is alive an considered accessable.
*/
ALIVE {
@Override
public Deleted delete() {
return TRASH;
}
},
/**
* The object is considered trashed but not deleted eventually.
*/
TRASH,
/**
* The object is deleted.
*/
DELETED {
/*
* (non-Javadoc)
* @see org.springframework.data.repository.Deleted#restore()
*/
@Override
public Deleted restore() {
return TRASH;
}
};
/*
* (non-Javadoc)
* @see org.springframework.data.jpa.repository.support.DeleteStates#activeValue()
*/
public Deleted activeValue() {
return ALIVE;
}
/*
* (non-Javadoc)
* @see org.springframework.data.jpa.repository.support.DeleteStates#delete()
*/
public Deleted delete() {
return DELETED;
}
/*
* (non-Javadoc)
* @see org.springframework.data.jpa.repository.support.DeleteStates#restore()
*/
public Deleted restore() {
return ALIVE;
}
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2013 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;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to enable soft-delete handling for entities of a given repository.
*
* @since 1.6
* @author Oliver Gierke
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
public @interface SoftDelete {
/**
* The name of the property that holds the deleted state.
*
* @return
*/
String value();
/**
* The mode of the property representing the deleted state. By default we assume a boolean flag being set to
* {@literal true} in case the entity shall be considered deleted.
*
* @return
*/
FlagMode flagMode() default FlagMode.DELETED;
/**
* Various strategies of how to interpret the entity's property capturing the deleted state.
*
* @since 1.6
* @author Oliver Gierke
*/
public enum FlagMode {
/**
* The flag in the domain type represents the active state. Thus, {@literal true} means it's active,
* {@literal false} is considered inactive or deleted. The opposite of {@link FlagMode#DELETED}. Expects the object
* property to be of type {@link Boolean} or {@literal boolean}.
*/
ACTIVE,
/**
* The flag in the domain type represents the deleted state. Thus {@literal true} means it's deleted,
* {@literal false} means the object is active. The opposite of {@link FlagMode#ACTIVE}. Expects the object property
* to be of type {@link Boolean} or {@literal boolean}.
*/
DELETED {
/*
* (non-Javadoc)
* @see org.springframework.data.repository.SoftDelete.FlagMode#toDeletedValue(java.lang.Object)
*/
@Override
public Object toDeletedValue(Object currentValue) {
return true;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.SoftDelete.FlagMode#activeValue()
*/
@Override
public Object activeValue() {
return false;
}
},
/**
* This strategy expects the property type of the value expresing the deleted state to implement
* {@link DeleteStates} and delegate to {@link DeleteStates#delete()} to determine the next value to be set when
* attempting a delete. The easiest way is to use {@link Deleted} but you can essentially implement any custom type
* that follows the spec defined in {@link DeleteStates}.
*
* @see Deleted
*/
TRASHABLE {
/*
* (non-Javadoc)
* @see org.springframework.data.repository.SoftDelete.FlagMode#toDeletedValue(java.lang.Object)
*/
@Override
public Object toDeletedValue(Object currentValue) {
if (currentValue == null) {
return null;
}
if (!(currentValue instanceof Deleted)) {
throw new IllegalArgumentException("Trashable flag mode only supports values of type Deleted!");
}
return ((Deleted) currentValue).delete();
}
};
/**
* Returns the value that represents that the entity is active.
*
* @return
*/
public Object activeValue() {
return true;
}
/**
* Returns the value to be used to express the entity being deleted. This can happen over a variety of stages, thus
* we need the current value to potentially determine the next.
*
* @param currentValue can be {@literal null}.
* @return
*/
public Object toDeletedValue(Object currentValue) {
return false;
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2013 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.augment;
import org.springframework.beans.ConfigurablePropertyAccessor;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyAccessor;
import org.springframework.data.repository.SoftDelete;
import org.springframework.validation.DataBinder;
/**
* Base class to implement a {@link QueryAugmentor} to soft-delete entities.
*
* @since 1.6
* @author Oliver Gierke
*/
public abstract class AbstractSoftDeleteQueryAugmentor<Q extends QueryContext<?>, N extends QueryContext<?>, U extends UpdateContext<?>>
extends AnnotationBasedQueryAugmentor<SoftDelete, Q, N, U> {
/*
* (non-Javadoc)
* @see org.springframework.data.repository.augment.AnnotationBasedQueryAugmentor#prepareUpdate(org.springframework.data.repository.augment.UpdateContext, java.lang.annotation.Annotation)
*/
@Override
protected U prepareUpdate(U context, SoftDelete annotation) {
if (!context.getMode().in(UpdateContext.UpdateMode.DELETE)) {
return context;
}
String property = annotation.value();
Object entity = context.getEntity();
if (entity == null) {
return context;
}
CustomDataBinder binder = new CustomDataBinder(entity);
binder.initDirectFieldAccess();
Object currentValue = binder.getPropertyAccessor().getPropertyValue(property);
Object nextValue = annotation.flagMode().toDeletedValue(currentValue);
if (nextValue == null) {
return context;
}
MutablePropertyValues values = new MutablePropertyValues();
values.add(property, nextValue);
binder.bind(values);
updateDeletedState(entity, context);
return null;
}
/**
* Update the entity using the API exposed in the given {@link UpdateContext}.
*
* @param entity will never be {@literal null}.
* @param context will never be {@literal null}.
*/
public abstract void updateDeletedState(Object entity, U context);
/**
* Custom {@link DataBinder} to expose the {@link PropertyAccessor} used.
*
* @author Oliver Gierke
*/
private static class CustomDataBinder extends DataBinder {
public CustomDataBinder(Object target) {
super(target);
}
@Override
public ConfigurablePropertyAccessor getPropertyAccessor() {
return super.getPropertyAccessor();
}
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2013 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.augment;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.repository.augment.QueryContext.QueryMode;
import org.springframework.data.repository.core.EntityMetadata;
/**
* Base implementation of {@link QueryAugmentor} to lookup an annotation on the repository method invoked or at the
* repository interface. It caches the lookups to avoid repeated reflection calls and hands the annotation found into
* {@link #prepareQuery(QueryContext, Annotation)} and {@link #prepareUpdate(UpdateContext, Annotation)} methods. Opts
* out of augmentation in case the annotation cannot be found on the method invoked or in the type.
*
* @since 1.6
* @author Oliver Gierke
*/
public abstract class AnnotationBasedQueryAugmentor<T extends Annotation, Q extends QueryContext<?>, N extends QueryContext<?>, U extends UpdateContext<?>>
implements QueryAugmentor<Q, N, U> {
private final Map<Method, T> cache = new HashMap<Method, T>();
private final Class<T> annotationType;
/**
* Creates a new {@link AnnotationBasedQueryAugmentor}.
*/
@SuppressWarnings("unchecked")
public AnnotationBasedQueryAugmentor() {
this.annotationType = (Class<T>) GenericTypeResolver.resolveTypeArguments(getClass(),
AnnotationBasedQueryAugmentor.class)[0];
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.QueryAugmentor#supports(org.springframework.data.repository.core.support.MethodMetadata, org.springframework.data.repository.core.support.QueryContext.Mode, org.springframework.data.repository.core.EntityMetadata)
*/
public boolean supports(MethodMetadata method, QueryMode queryMode, EntityMetadata<?> entityMetadata) {
if (cache.containsKey(method)) {
return cache.get(method) == null;
}
return findAndCacheAnnotation(method) != null;
}
/**
* Finds the annotation using the given {@link MethodMetadata} and caches it if found.
*
* @param metadata must not be {@literal null}.
* @return
*/
private T findAndCacheAnnotation(MethodMetadata metadata) {
Method method = metadata.getMethod();
T expression = AnnotationUtils.findAnnotation(method, annotationType);
if (expression != null) {
cache.put(method, expression);
return expression;
}
for (Class<?> type : metadata.getInvocationTargetType()) {
expression = findAndCache(type, method);
if (expression != null) {
return expression;
}
}
return findAndCache(method.getDeclaringClass(), method);
}
/**
* Tries to find the annotation on the given type and caches it if found.
*
* @param type must not be {@literal null}.
* @param method must not be {@literal null}.
* @return
*/
private T findAndCache(Class<?> type, Method method) {
T expression = AnnotationUtils.findAnnotation(type, annotationType);
if (expression != null) {
cache.put(method, expression);
return expression;
}
return null;
}
public final N augmentNativeQuery(N context, MethodMetadata metadata) {
return null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.jpa.repository.support.JpaQueryAugmentor#augmentQuery(javax.persistence.criteria.CriteriaQuery, org.springframework.data.repository.core.support.MethodMetadata)
*/
public final Q augmentQuery(Q context, MethodMetadata metadata) {
Method method = metadata.getMethod();
if (cache.containsKey(method)) {
return prepareQuery(context, cache.get(method));
}
T expression = findAndCacheAnnotation(metadata);
return expression == null ? context : prepareQuery(context, expression);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.QueryAugmentor#augmentUpdate(org.springframework.data.repository.core.support.UpdateContext, org.springframework.data.repository.core.support.MethodMetadata)
*/
public final U augmentUpdate(U update, MethodMetadata metadata) {
Method method = metadata.getMethod();
if (cache.containsKey(method)) {
return prepareUpdate(update, cache.get(method));
}
T expression = findAndCacheAnnotation(metadata);
return expression == null ? update : prepareUpdate(update, cache.get(method));
}
protected N prepareNativeQuery(N context, T expression) {
return context;
}
/**
* Prepare the query contained in the given {@link QueryContext} using the given annotation. Default implementation
* returns the context as is.
*
* @param context will never be {@literal null}.
* @param expression will never be {@literal null}.
* @return
*/
protected Q prepareQuery(Q context, T expression) {
return context;
}
/**
* Prepare the update contained in the given {@link UpdateContext} using the given annotation. Default implementation
* returns the context as is.
*
* @param context will never be {@literal null}.
* @param annotation will never be {@literal null}.
* @return
*/
protected U prepareUpdate(U context, T annotation) {
return context;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2013 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.augment;
import java.lang.reflect.Method;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
/**
* Interface to abstract {@link MethodMetadata} to be looked up for the repository method invoked.
*
* @see 1.6
* @author Oliver Gierke
*/
public interface MethodMetadata {
/**
* Returns the arguments piped into the current repository method invocation.
*
* @return
*/
Object[] getInvocationArguments();
/**
* Returns the type about to be invoked. This will usually be the type of the repository invoked, even if the method
* is actually declared in a Spring Data repository interface (e.g. {@link CrudRepository}).
*
* @return
*/
List<Class<?>> getInvocationTargetType();
/**
* Returns the method invoked at the repository level.
*
* @return
*/
Method getMethod();
}

View File

@@ -0,0 +1,188 @@
/*
* Copyright 2013 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.augment;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.data.repository.augment.QueryContext.QueryMode;
import org.springframework.data.repository.core.EntityMetadata;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Wrapper for a collection of {@link QueryAugmentor}s. Groups them by the context type they're referring to for the
* appropriate invocation later on.
*
* @since 1.6
* @author Oliver Gierke
*/
public class QueryAugmentationEngine {
private static final Iterable<QueryAugmentor<? extends QueryContext<?>, ? extends QueryContext<?>, ? extends UpdateContext<?>>> NO_AUGMENTORS = Collections
.emptySet();
public static final QueryAugmentationEngine NONE = new QueryAugmentationEngine(NO_AUGMENTORS, null, false);
private static final Logger LOGGER = LoggerFactory.getLogger(QueryAugmentationEngine.class);
private static final Comparator<Object> COMPARATOR = new AnnotationAwareOrderComparator();
private final MultiValueMap<Class<?>, QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>>> augmentors = //
new LinkedMultiValueMap<Class<?>, QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>>>();
private final MethodMetadata methodMetadata;
/**
* Creates a new {@link QueryAugmentationEngine} by inspecting the given {@link QueryAugmentor}s.
*
* @param augmentors must not be {@literal null}.
* @param metadataProvider must not be {@literal null}.
*/
public QueryAugmentationEngine(
Iterable<QueryAugmentor<? extends QueryContext<?>, ? extends QueryContext<?>, ? extends UpdateContext<?>>> augmentors,
MethodMetadata metadataProvider) {
this(augmentors, metadataProvider, true);
}
/**
* Internal constructor to allow {@link #NONE} being created with a {@literal null} {@link MethodMetadata} which
* actually must not be null otherwise.
*
* @param augmentors the {@link QueryAugmentor}s to register.
* @param methodMetadata
* @param checkNull whether to check the {@link MethodMetadata} for {@literal null}.
*/
@SuppressWarnings("unchecked")
private QueryAugmentationEngine(
Iterable<QueryAugmentor<? extends QueryContext<?>, ? extends QueryContext<?>, ? extends UpdateContext<?>>> augmentors,
MethodMetadata methodMetadata, boolean checkNull) {
Assert.notNull(augmentors, "QueryAugmentors must not be null!");
if (checkNull) {
Assert.notNull(methodMetadata, "MethodMetadata must not be null!");
}
this.methodMetadata = methodMetadata;
for (QueryAugmentor<? extends QueryContext<?>, ? extends QueryContext<?>, ? extends UpdateContext<?>> augmentor : augmentors) {
Class<?>[] keys = GenericTypeResolver.resolveTypeArguments(augmentor.getClass(), QueryAugmentor.class);
QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>> castedAugmentor = (QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>>) augmentor;
this.augmentors.add(keys[0], castedAugmentor);
this.augmentors.add(keys[1], castedAugmentor);
this.augmentors.add(keys[2], castedAugmentor);
}
for (List<QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>>> values : this.augmentors.values()) {
Collections.sort(values, COMPARATOR);
}
}
/**
* Returns whether there's any {@link QueryAugmentor} registered to be invoked for the given context.
*
* @param contextType the context type about to be handled.
* @param queryMode the execution mode.
* @param metadata the {@link EntityMetadata}.
* @return
*/
public boolean augmentationNeeded(Class<?> contextType, QueryMode queryMode, EntityMetadata<?> metadata) {
if (!augmentors.containsKey(contextType)) {
return false;
}
for (QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>> augmentor : augmentors.get(contextType)) {
if (augmentor.supports(methodMetadata, queryMode, metadata)) {
return true;
}
}
return false;
}
public <T extends QueryContext<?>> T invokeNativeAugmentors(T context) {
return invokeAugmentor(new AugmentorInvoker<T>() {
@SuppressWarnings("unchecked")
public T invokeAugmentor(QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>> augmentor, T context) {
return (T) augmentor.augmentNativeQuery(context, methodMetadata);
}
}, context);
}
/**
* Invokes all {@link QueryAugmentor}s registered for the given {@link QueryContext}.
*
* @param context
*/
public <N extends QueryContext<?>> N invokeAugmentors(N context) {
return invokeAugmentor(new AugmentorInvoker<N>() {
@SuppressWarnings("unchecked")
public N invokeAugmentor(QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>> augmentor, N context) {
return (N) augmentor.augmentQuery(context, methodMetadata);
}
}, context);
}
/**
* Invokes the registered {@link QueryAugmentor}s using the given {@link UpdateContext}.
*
* @param context must not be {@literal null}.
* @return
*/
public <U extends UpdateContext<?>> U invokeAugmentors(U context) {
return invokeAugmentor(new AugmentorInvoker<U>() {
@SuppressWarnings("unchecked")
public U invokeAugmentor(QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>> augmentor, U context) {
return (U) augmentor.augmentUpdate(context, methodMetadata);
}
}, context);
}
private <T> T invokeAugmentor(AugmentorInvoker<T> invoker, T context) {
Assert.notNull(context, "UpdateContext must not be null!");
T augmentedContext = context;
for (QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>> augmentor : augmentors.get(context
.getClass())) {
LOGGER.debug("Invoking augmentor {} for context {}", augmentor, context);
augmentedContext = invoker.invokeAugmentor(augmentor, augmentedContext);
if (augmentedContext == null) {
return null;
}
}
return augmentedContext;
}
interface AugmentorInvoker<T> {
T invokeAugmentor(QueryAugmentor<QueryContext<?>, QueryContext<?>, UpdateContext<?>> augmentor, T context);
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2013 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.augment;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
/**
* Injection interface to express the dependency to a {@link QueryAugmentationEngine}. Usually implemented by repository
* implementations. The {@link RepositoryFactorySupport} base class will inject the {@link QueryAugmentationEngine}.
*
* @since 1.6
* @author Oliver Gierke
*/
public interface QueryAugmentationEngineAware {
/**
* Configures the {@link QueryAugmentationEngine} to be used.
*
* @param engine will never be {@literal null}.
*/
void setQueryAugmentationEngine(QueryAugmentationEngine engine);
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2013 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.augment;
import org.springframework.data.repository.augment.QueryContext.QueryMode;
import org.springframework.data.repository.core.EntityMetadata;
/**
* SPI to abstract components that want to augment queries executed by the repositories.
*
* @since 1.6
* @author Oliver Gierke
*/
public interface QueryAugmentor<Q extends QueryContext<?>, N extends QueryContext<?>, S extends UpdateContext<?>> {
/**
* Determines whether the implementation is interested in augmentation at all. The implementations can expect to only
* get {@link #augmentQuery(QueryContext, MethodMetadata)} and {@link #augmentUpdate(UpdateContext, MethodMetadata)}
* after the client has called this method and the implementation returned {@literal true}.
*
* @param method metadata about the repository method invoked, will never be {@literal null}.
* @param queryMode the query execution mode, will never be {@literal null}.
* @param entityMetadata metadata about the entity, will never be {@literal null}. Useful to create type specific
* augmentors.
* @return
*/
boolean supports(MethodMetadata method, QueryMode queryMode, EntityMetadata<?> entityMetadata);
N augmentNativeQuery(N query, MethodMetadata methodMetadata);
/**
* Augments the query by either adding further constraints to it or entirely replacing it. Clients will use
* {@link QueryContext#getQuery()} proceeding with the query execution.
*
* @param query the query context of the query about to be executed.
* @param methodMetadata metadata about the repository method being invoked.
* @return must not be {@literal null}.
*/
Q augmentQuery(Q query, MethodMetadata methodMetadata);
/**
* Augments the update about to be executed. Implementations can prevent the original update from being executed by
* returning null.
*
* @param update the update context of the update about to be executed
* @param methodMetadata metadata about the repository method being invoked.
* @return the update to continue to work with or {@literal null} in case the implementation already executed custom
* update and wants to prevent the execution of the original update.
*/
S augmentUpdate(S update, MethodMetadata methodMetadata);
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2013 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.augment;
import org.springframework.util.Assert;
/**
* The context of a query to be executed. Holding the actual query as well as a queryMode to express in which context
* the query shall be executed.
*
* @since 1.6
* @author Oliver Gierke
*/
public class QueryContext<T> {
/**
* The mode of the query execution.
*
* @since 1.6
* @author Oliver Gierke
*/
public static enum QueryMode {
/**
* To be used for queries originating from find methods of CRUD functionality.
*/
FIND,
/**
* TO be used for queries executed for exist queries.
*/
EXIST,
/**
* To be used for the query to be executed.
*/
COUNT,
/**
* To be used for the execution of custom query methods.
*/
QUERY,
/**
* To be used for the execution of the additional count query to be executed when paging.
*/
COUNT_FOR_PAGING;
/**
* Returns whether the {@link QueryMode} is one of the given ones.
*
* @param modes must not be {@literal null}.
* @return
*/
public boolean in(QueryMode... modes) {
for (QueryMode queryMode : modes) {
if (queryMode == this) {
return true;
}
}
return false;
}
}
private final T query;
private final QueryMode queryMode;
/**
* Creates a new {@link QueryContext}.
*
* @param query the query about to be executed, must not be {@literal null}.
* @param queryMode the queryMode in which the query shall be executed, must not be {@literal null}.
*/
public QueryContext(T query, QueryMode queryMode) {
Assert.notNull(query, "Query must not be null!");
Assert.notNull(queryMode, "QueryMode must not be null!");
this.query = query;
this.queryMode = queryMode;
}
/**
* Returns the query to be executed.
*
* @return the query
*/
public T getQuery() {
return query;
}
/**
* Returns the execution queryMode.
*
* @return the queryMode
*/
public QueryMode getMode() {
return queryMode;
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2013 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.augment;
import org.springframework.util.Assert;
/**
* Context to be handed around for update executions.
*
* @since 1.6
* @author Oliver Gierke
*/
public class UpdateContext<T> {
/**
* The mode of the update execution.
*
* @since 1.6
* @author Oliver Gierke
*/
public static enum UpdateMode {
/**
* To be used for methods saving an object.
*/
SAVE,
/**
* To bes used for methods deleting an object.
*/
DELETE,
/**
* To be used when executing modifying queries.
*/
MODIFIYING_QUERY;
/**
* Returns whether the {@link UpdateMode} is one of the given ones.
*
* @param modes must not be {@literal null}.
* @return
*/
public boolean in(UpdateMode... modes) {
for (UpdateMode updateMode : modes) {
if (updateMode == this) {
return true;
}
}
return false;
}
}
private final T entity;
private final UpdateMode updateMode;
/**
* Creates a new {@link UpdateContext} for the given entity and {@link UpdateContext}.
*
* @param entity must not be {@literal null}.
* @param updateMode must not be {@literal null}.
*/
public UpdateContext(T entity, UpdateMode updateMode) {
Assert.notNull(entity, "Entity must not be null!");
Assert.notNull(updateMode, "QueryMode must not be null!");
this.entity = entity;
this.updateMode = updateMode;
}
/**
* Returns the entity to be updated.
*
* @return will never be {@literal null}.
*/
public T getEntity() {
return entity;
}
/**
* Returns the execution mode.
*
* @return will never be {@literal null}.
*/
public UpdateMode getMode() {
return updateMode;
}
}

View File

@@ -0,0 +1,4 @@
/**
* Implementation of the query augmentation mechanism.
*/
package org.springframework.data.repository.augment;

View File

@@ -19,6 +19,8 @@ import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -26,7 +28,9 @@ import java.util.concurrent.ConcurrentHashMap;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.framework.ReflectiveMethodInvocation;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanClassLoaderAware;
@@ -35,6 +39,12 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.augment.MethodMetadata;
import org.springframework.data.repository.augment.QueryAugmentationEngine;
import org.springframework.data.repository.augment.QueryAugmentationEngineAware;
import org.springframework.data.repository.augment.QueryAugmentor;
import org.springframework.data.repository.augment.QueryContext;
import org.springframework.data.repository.augment.UpdateContext;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
@@ -74,9 +84,11 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
private EvaluationContextProvider evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE;
private QueryCollectingQueryCreationListener collectingListener = new QueryCollectingQueryCreationListener();
private QueryAugmentationEngine augmentationEngine = QueryAugmentationEngine.NONE;
public RepositoryFactorySupport() {
this.queryPostProcessors.add(collectingListener);
this.postProcessors.add(MethodMetadataRepositoryProxyPostProcessor.INSTANCE);
}
/**
@@ -117,6 +129,16 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
: evaluationContextProvider;
}
/**
* Configures the {@link QueryAugmentor}s to be used with the repository instances about to be created.
*
* @param augmentors must not be {@literal null}.
*/
public void setQueryAugmentors(
List<QueryAugmentor<? extends QueryContext<?>, ? extends QueryContext<?>, ? extends UpdateContext<?>>> augmentors) {
this.augmentationEngine = new QueryAugmentationEngine(augmentors, DefaultMethodMetadata.INSTANCE);
}
/**
* Configures the repository base class to use when creating the repository proxy. If not set, the factory will use
* the type returned by {@link #getRepositoryBaseClass(RepositoryMetadata)} by default.
@@ -184,6 +206,10 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
Object target = getTargetRepository(information);
if (target instanceof QueryAugmentationEngineAware) {
((QueryAugmentationEngineAware) target).setQueryAugmentationEngine(augmentationEngine);
}
// Create proxy
ProxyFactory result = new ProxyFactory();
result.setTarget(target);
@@ -595,4 +621,69 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware {
return result;
}
}
/**
* {@link RepositoryProxyPostProcessor} registering an {@link ExposeInvocationInterceptor} to make the repository
* level method invocation available to the infrastructure.
*
* @author Oliver Gierke
*/
private static enum MethodMetadataRepositoryProxyPostProcessor implements RepositoryProxyPostProcessor {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryProxyPostProcessor#postProcess(org.springframework.aop.framework.ProxyFactory, org.springframework.data.repository.core.RepositoryInformation)
*/
public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) {
factory.addAdvice(ExposeInvocationInterceptor.INSTANCE);
}
}
/**
* Default implementation of {@link MethodMetadata}.
*
* @author Oliver Gierke
*/
private static enum DefaultMethodMetadata implements MethodMetadata {
INSTANCE;
private MethodInvocation getMethodInvocation() {
return ExposeInvocationInterceptor.currentInvocation();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.MethodMetadata#getInvocationTargetType()
*/
public List<Class<?>> getInvocationTargetType() {
MethodInvocation invocation = getMethodInvocation();
if (invocation instanceof ReflectiveMethodInvocation) {
Advised proxy = (Advised) ((ReflectiveMethodInvocation) invocation).getProxy();
return Arrays.asList((Class<?>[]) proxy.getProxiedInterfaces());
}
return Collections.<Class<?>> singletonList(getMethodInvocation().getThis().getClass());
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.MethodMetadata#getInvocationArguments()
*/
public Object[] getInvocationArguments() {
return getMethodInvocation().getArguments();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.MethodMetadata#getMethod()
*/
public Method getMethod() {
return getMethodInvocation().getMethod();
}
}
}

View File

@@ -80,7 +80,7 @@ class TransactionalRepositoryProxyPostProcessor implements RepositoryProxyPostPr
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryProxyPostProcessor#postProcess(org.springframework.aop.framework.ProxyFactory, org.springframework.data.repository.core.RepositoryInformation)
* @see org.springframework.data.repository.core.support.RepositoryProxyPostProcessor#postProcess(org.springframework.aop.framework.ProxyFactory)
*/
public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) {