diff --git a/pom.xml b/pom.xml index e0af6454e..1a16b2981 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,11 @@ - + 4.0.0 - + org.springframework.data spring-data-commons - 1.12.0.BUILD-SNAPSHOT + 1.12.0.DATACMNS-293-SNAPSHOT Spring Data Core @@ -131,7 +131,7 @@ provided true - + com.google.guava guava @@ -199,7 +199,6 @@ - com.mysema.maven apt-maven-plugin @@ -217,7 +216,6 @@ - org.apache.maven.plugins maven-enforcer-plugin @@ -251,7 +249,6 @@ org.asciidoctor asciidoctor-maven-plugin - diff --git a/src/main/java/org/springframework/data/repository/DeleteStates.java b/src/main/java/org/springframework/data/repository/DeleteStates.java new file mode 100644 index 000000000..68c2cd4bf --- /dev/null +++ b/src/main/java/org/springframework/data/repository/DeleteStates.java @@ -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 { + + /** + * 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(); +} diff --git a/src/main/java/org/springframework/data/repository/Deleted.java b/src/main/java/org/springframework/data/repository/Deleted.java new file mode 100644 index 000000000..17e2136ea --- /dev/null +++ b/src/main/java/org/springframework/data/repository/Deleted.java @@ -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 { + + /** + * 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; + } +} diff --git a/src/main/java/org/springframework/data/repository/SoftDelete.java b/src/main/java/org/springframework/data/repository/SoftDelete.java new file mode 100644 index 000000000..3b6811837 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/SoftDelete.java @@ -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; + } + } +} diff --git a/src/main/java/org/springframework/data/repository/augment/AbstractSoftDeleteQueryAugmentor.java b/src/main/java/org/springframework/data/repository/augment/AbstractSoftDeleteQueryAugmentor.java new file mode 100644 index 000000000..38096fd88 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/augment/AbstractSoftDeleteQueryAugmentor.java @@ -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, N extends QueryContext, U extends UpdateContext> + extends AnnotationBasedQueryAugmentor { + + /* + * (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(); + } + } +} diff --git a/src/main/java/org/springframework/data/repository/augment/AnnotationBasedQueryAugmentor.java b/src/main/java/org/springframework/data/repository/augment/AnnotationBasedQueryAugmentor.java new file mode 100644 index 000000000..62405cdf1 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/augment/AnnotationBasedQueryAugmentor.java @@ -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, N extends QueryContext, U extends UpdateContext> + implements QueryAugmentor { + + private final Map cache = new HashMap(); + private final Class annotationType; + + /** + * Creates a new {@link AnnotationBasedQueryAugmentor}. + */ + @SuppressWarnings("unchecked") + public AnnotationBasedQueryAugmentor() { + this.annotationType = (Class) 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; + } +} diff --git a/src/main/java/org/springframework/data/repository/augment/MethodMetadata.java b/src/main/java/org/springframework/data/repository/augment/MethodMetadata.java new file mode 100644 index 000000000..9f74819d9 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/augment/MethodMetadata.java @@ -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> getInvocationTargetType(); + + /** + * Returns the method invoked at the repository level. + * + * @return + */ + Method getMethod(); +} diff --git a/src/main/java/org/springframework/data/repository/augment/QueryAugmentationEngine.java b/src/main/java/org/springframework/data/repository/augment/QueryAugmentationEngine.java new file mode 100644 index 000000000..98b29c828 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/augment/QueryAugmentationEngine.java @@ -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, ? 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 COMPARATOR = new AnnotationAwareOrderComparator(); + + private final MultiValueMap, QueryAugmentor, QueryContext, UpdateContext>> augmentors = // + new LinkedMultiValueMap, QueryAugmentor, 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, ? 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, ? 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 UpdateContext> augmentor : augmentors) { + + Class[] keys = GenericTypeResolver.resolveTypeArguments(augmentor.getClass(), QueryAugmentor.class); + QueryAugmentor, QueryContext, UpdateContext> castedAugmentor = (QueryAugmentor, QueryContext, UpdateContext>) augmentor; + + this.augmentors.add(keys[0], castedAugmentor); + this.augmentors.add(keys[1], castedAugmentor); + this.augmentors.add(keys[2], castedAugmentor); + } + + for (List, 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, UpdateContext> augmentor : augmentors.get(contextType)) { + if (augmentor.supports(methodMetadata, queryMode, metadata)) { + return true; + } + } + + return false; + } + + public > T invokeNativeAugmentors(T context) { + + return invokeAugmentor(new AugmentorInvoker() { + @SuppressWarnings("unchecked") + public T invokeAugmentor(QueryAugmentor, 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 invokeAugmentors(N context) { + + return invokeAugmentor(new AugmentorInvoker() { + @SuppressWarnings("unchecked") + public N invokeAugmentor(QueryAugmentor, 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 invokeAugmentors(U context) { + + return invokeAugmentor(new AugmentorInvoker() { + @SuppressWarnings("unchecked") + public U invokeAugmentor(QueryAugmentor, QueryContext, UpdateContext> augmentor, U context) { + return (U) augmentor.augmentUpdate(context, methodMetadata); + } + }, context); + } + + private T invokeAugmentor(AugmentorInvoker invoker, T context) { + + Assert.notNull(context, "UpdateContext must not be null!"); + T augmentedContext = context; + + for (QueryAugmentor, 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 invokeAugmentor(QueryAugmentor, QueryContext, UpdateContext> augmentor, T context); + } +} diff --git a/src/main/java/org/springframework/data/repository/augment/QueryAugmentationEngineAware.java b/src/main/java/org/springframework/data/repository/augment/QueryAugmentationEngineAware.java new file mode 100644 index 000000000..1ef828dfb --- /dev/null +++ b/src/main/java/org/springframework/data/repository/augment/QueryAugmentationEngineAware.java @@ -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); +} diff --git a/src/main/java/org/springframework/data/repository/augment/QueryAugmentor.java b/src/main/java/org/springframework/data/repository/augment/QueryAugmentor.java new file mode 100644 index 000000000..d5844b2c9 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/augment/QueryAugmentor.java @@ -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, 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); +} diff --git a/src/main/java/org/springframework/data/repository/augment/QueryContext.java b/src/main/java/org/springframework/data/repository/augment/QueryContext.java new file mode 100644 index 000000000..e4339b3da --- /dev/null +++ b/src/main/java/org/springframework/data/repository/augment/QueryContext.java @@ -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 { + + /** + * 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; + } +} diff --git a/src/main/java/org/springframework/data/repository/augment/UpdateContext.java b/src/main/java/org/springframework/data/repository/augment/UpdateContext.java new file mode 100644 index 000000000..d621ac172 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/augment/UpdateContext.java @@ -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 { + + /** + * 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; + } +} diff --git a/src/main/java/org/springframework/data/repository/augment/package-info.java b/src/main/java/org/springframework/data/repository/augment/package-info.java new file mode 100644 index 000000000..5c1e5131b --- /dev/null +++ b/src/main/java/org/springframework/data/repository/augment/package-info.java @@ -0,0 +1,4 @@ +/** + * Implementation of the query augmentation mechanism. + */ +package org.springframework.data.repository.augment; \ No newline at end of file diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index 56c5ae52b..97805dba1 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -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, ? 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> getInvocationTargetType() { + + MethodInvocation invocation = getMethodInvocation(); + + if (invocation instanceof ReflectiveMethodInvocation) { + Advised proxy = (Advised) ((ReflectiveMethodInvocation) invocation).getProxy(); + return Arrays.asList((Class[]) proxy.getProxiedInterfaces()); + } + + return Collections.> 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(); + } + } } diff --git a/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryProxyPostProcessor.java b/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryProxyPostProcessor.java index bff3e6faa..0e416d9dc 100644 --- a/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryProxyPostProcessor.java +++ b/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryProxyPostProcessor.java @@ -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) {